I am new to Espresso, and trying to write a test on a fragment that makes a Retrofit call when it is instantiated. When the call is received, I want to check the fragment to see if a view exists. I'm using an IdlingResource for the fragment and setting up a listener to be called that transitions the ResourceCallback to idle when the response is received (followed some steps in the implementation here: https://stackoverflow.com/a/30820189/4102823).
My fragment is instantiated when the user logs in and starts up MainActivity. The problem is that I just don't think my IdlingResource is set up correctly, and I don't know what's wrong with it. It is not even constructed until after the fragment is initiated and the call is made, even though I'm registering the IdlingResource in the test's setUp() method before everything else. So, I think the main issue here is how I get the IdlingResource instantiated alongside the fragment when I run the test, not after it. Could the problem be in the #Rule? Does it start MainActivity (which creates a NewsfeedFragment instance) before the test can run on the fragment? If so, how would I use a rule with my fragment instead?
Here is my fragment:
public ProgressListener mProgressListener;
public boolean mIsProgressShown = true;
public interface ProgressListener {
void onProgressShown();
void onProgressDismissed();
}
public void setProgressListener(ProgressListener progressListener) {
mProgressListener = progressListener;
}
public NewsfeedFragment() {
}
public static NewsfeedFragment newInstance() {
return new NewsfeedFragment();
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
OSUtil.setTranslucentStatusBar(getActivity().getWindow(), this.getContext());
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
mRootView = (SwipeRefreshLayout) inflater.inflate(R.layout.fragment_newsfeed, container, false);
mNewsfeedFramelayout = (FrameLayout) mRootView.findViewById(R.id.newsfeed_framelayout);
mProgressView = (ProgressBar) mRootView.findViewById(R.id.newsfeed_progress);
mPageIndictorView = (FrameLayout) mRootView.findViewById(R.id.page_indicator);
mPageIndicator = (TextView) mRootView.findViewById(R.id.page_indicator_text);
mRootView.setOnRefreshListener(this);
// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) mRootView.findViewById(R.id.newsfeed_viewpager);
mPageChangeListener = this;
if (savedInstanceState != null) {
mTabPosition = savedInstanceState.getInt(EXTRA_TAB_POSITION);
mViewPager.setCurrentItem(mTabPosition);
}
fetchNewsFeed();
return mRootView;
}
private void fetchNewsFeed() {
if (NetworkUtils.isConnectedToInternet(getActivity())) {
if (NetworkUtils.getService() != null) {
if (mRootView.isRefreshing()) {
dismissProgress();
} else {
showProgress();
}
showProgress();
Call<Newsfeed> call = NetworkUtils.getService().getNewsfeed();
call.enqueue(new Callback<Newsfeed>() {
#Override
public void onResponse(Call<Newsfeed> call, Response<Newsfeed> response) {
if (response.isSuccessful()) {
dismissProgress();
mNewsfeed = response.body();
FragmentManager fragmentManager = getChildFragmentManager();
mNewsfeedPagerAdapter = new NewsfeedPagerAdapter(fragmentManager, mNewsfeed.getNewsfeedItems());
}
...
private void showProgress() {
// show the progress and notify the listener
if (mProgressListener != null){
setProgressIndicatorVisible(true);
notifyListener(mProgressListener);
}
}
public void dismissProgress() {
// hide the progress and notify the listener
if (mProgressListener != null){
setProgressIndicatorVisible(false);
mIsProgressShown = false;
notifyListener(mProgressListener);
}
}
public boolean isInProgress() {
return mIsProgressShown;
}
private void notifyListener(ProgressListener listener) {
if (listener == null){
return;
}
if (isInProgress()){
listener.onProgressShown();
}
else {
listener.onProgressDismissed();
}
}
Here is the IdlingResource:
public class ProgressIdlingResource implements IdlingResource, NewsfeedFragment.ProgressListener {
private ResourceCallback mResourceCallback;
private NewsfeedFragment mNewsfeedFragment;
public ProgressIdlingResource(NewsfeedFragment fragment) {
mNewsfeedFragment = fragment;
mNewsfeedFragment.setProgressListener(this);
}
#Override
public String getName() {
return "ProgressIdlingResource";
}
#Override
public boolean isIdleNow() {
return !mNewsfeedFragment.mIsProgressShown;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
mResourceCallback = callback;
}
#Override
public void onProgressShown() {
}
#Override
public void onProgressDismissed() {
if (mResourceCallback == null) {
return;
}
//Called when the resource goes from busy to idle.
mResourceCallback.onTransitionToIdle();
}
}
The fragment test:
public class NewsfeedFragmentTest {
#Before
public void setUp() throws Exception {
Espresso.registerIdlingResources(new ProgressIdlingResource((NewsfeedFragment) MainActivity.getCurrentFragment()));
}
#Rule
public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
#Test
public void getViewPager() throws Exception {
onView(allOf(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE),
withId(R.id.newsfeed_viewpager))).check(matches(isDisplayed()));
}
#Test
public void getNewsfeedItems() throws Exception {
onView(withId(R.id.page_indicator)).check(matches(isDisplayed()));
}
}
Retrofit is using OkHttp, and there is a standard way to setup IdlingResource for that matter. Refer to OkHttp IdlingResource
Related
I have the same UI-related features and implementations that I want to use once in Activity and again in Fragment. How do I implement the architecture to avoid duplicate code?
Does displaying a Fragment in an Activity reduce performance?
My goal is to implement an abstract class for other classes to use pagination. Child classes are sometimes Activity type and sometimes Fragment type.
I designed an interface that has the following methods. Then I implemented these features in the abstract Fragment class:
public interface PaginableComponent {
void onRequestList();
void onSuccessLoadList(#NonNull List<Paginable> list);
void onErrorLoadList(String errorMessage);
void onRequestLoadMoreList(int oldTotalCount);
void onSuccessLoadMore(#NonNull List<Paginable> list);
void onTryLoadList();
#NonNull
PaginableAdapter getAdapter();
}
This is the abstract Fragment class that implements this interface:
public abstract class PaginableFragment extends BaseFragment
implements PaginableComponent, PaginableAdapter.LoadListMoreListener
{
private final AtomicBoolean mIsFirstLoadedList = new
AtomicBoolean(false);
private SwipeRefreshLayout mSwipeRefreshLayout;
private View mStatusLayout;
private ProgressBar mStatusProgressBar;
private TextView mStatusMessageTextView;
private TextView mTryAgainTextView;
public Boolean isHidden = true;
#CallSuper
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initViews(view);
getAdapter().setLoadListMoreListener(this);
if (mSwipeRefreshLayout != null) {
mSwipeRefreshLayout.setColorSchemeColors(UiUtils.getStyleColor(requireContext(), R.attr.colorPrimary));
mSwipeRefreshLayout.setOnRefreshListener(this::checkNetAndRequestList);
}
}
#Override
public void onPause() {
super.onPause();
isHidden = true;
}
#Override
public void onResume() {
super.onResume();
isHidden = isHidden();
}
#Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
isHidden = hidden;
}
private void initViews(View view) {
mSwipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout);
mStatusLayout = view.findViewById(R.id.statusLayout);
if (mStatusLayout != null) {
mStatusProgressBar = mStatusLayout.findViewById(R.id.progressBar);
mStatusMessageTextView = mStatusLayout.findViewById(R.id.text);
mTryAgainTextView = mStatusLayout.findViewById(R.id.tryAgain);
mTryAgainTextView.setOnClickListener(v -> onTryLoadList());
}
}
protected void showStatus(String type) {
if (mStatusLayout != null) {
switch (type) {
case Messages.MSG_LOADING:
mStatusProgressBar.setVisibility(View.VISIBLE);
mStatusMessageTextView.setVisibility(View.GONE);
mTryAgainTextView.setVisibility(View.GONE);
break;
case Messages.MSG_NET:
mStatusMessageTextView.setText(R.string.no_network_connection);
mStatusProgressBar.setVisibility(View.GONE);
mStatusMessageTextView.setVisibility(View.VISIBLE);
mTryAgainTextView.setVisibility(View.VISIBLE);
break;
case Messages.MSG_ERROR:
mStatusMessageTextView.setText(R.string.sorry_unexpected_error_occurred);
mStatusProgressBar.setVisibility(View.GONE);
mStatusMessageTextView.setVisibility(View.VISIBLE);
mTryAgainTextView.setVisibility(View.GONE);
break;
}
mStatusLayout.setVisibility(View.VISIBLE);
}
}
private void hideStatus() {
if (mStatusLayout != null) {
mStatusLayout.setVisibility(View.GONE);
}
}
#CallSuper
#Override
public void onSuccessLoadList(#NonNull List<Paginable> list) {
mIsFirstLoadedList.set(true);
if (mSwipeRefreshLayout != null) {
mSwipeRefreshLayout.setRefreshing(false);
}
hideStatus();
getAdapter().resetList(list);
}
#CallSuper
#Override
public void onErrorLoadList(String errorMessage) {
if (mSwipeRefreshLayout != null) {
mSwipeRefreshLayout.setRefreshing(false);
}
showStatus(Messages.MSG_ERROR);
getAdapter().setIsLoadingMore(false);
}
#CallSuper
#Override
public void onLoadListMore(int oldTotalCount) {
getAdapter().showEndMessage(Paginable.TYPE_SHIMMER);
if (NetReceiver.isNetConnected(requireContext())) {
onRequestLoadMoreList(oldTotalCount);
} else {
showStatus(Messages.MSG_NET);
}
}
#CallSuper
#Override
public void onSuccessLoadMore(#NonNull List<Paginable> list) {
if (mSwipeRefreshLayout != null) {
mSwipeRefreshLayout.setRefreshing(false);
}
hideStatus();
getAdapter().hideEndMessages();
getAdapter().addList(list);
getAdapter().setIsLoadingMore(false);
}
#CallSuper
#Override
public void onTryLoadList() {
if (!mIsFirstLoadedList.get()) {
checkNetAndRequestList();
}
}
#Override
public void onNetEvent(NetReceiver.NetEvent event) {
if (event.isConnected()) {
hideStatus();
if (!mIsFirstLoadedList.get()) {
getAdapter().clearAndInitShimmers();
onRequestList();
}
if (getAdapter().isLoadingMore()) {
onRequestLoadMoreList(getAdapter().getItemCount());
}
} else {
showStatus(Messages.MSG_NET);
}
}
public void checkNetAndRequestList() {
if (NetReceiver.isNetConnected(requireContext())) {
if (!mIsFirstLoadedList.get()) {
getAdapter().clearAndInitShimmers();
}
onRequestList();
} else {
if (mSwipeRefreshLayout != null) {
mSwipeRefreshLayout.setRefreshing(false);
}
showStatus(Messages.MSG_NET);
if (!mIsFirstLoadedList.get()) {
getAdapter().clearAndInitNoInternet();
}
}
}
}
Now I want to use the same implementation in the activities.
One way to prevent duplicate code is to use ViewModel and LiveData in the MVVM architecture.
I have a difficulties with MVP realization using Moxy library. I read Moxy's github pages and checked examples, but any solution doesn't helps me.
In MyFragment the callbacks of MyFragmentView methods not called, but in MyFragmentPresenter getViewState() returns not null.
I mean that
getViewState().showProgressDialog();
getViewState().setAccounts(accountsResponse);
getViewState().hideProgressDialog();
are called in MyPresenter, but in MyFragment
#Override
public void showProgressDialog() {
// some code
}
#Override
public void hideProgressDialog() {
// some code
}
#Override
public void setAccounts(AccountsResponse accounts) {
// some code
}
doesn't called.
Please help, what's wrong?
My code below.
Gradle
compile 'com.arello-mobile:moxy:1.5.5'
compile 'com.arello-mobile:moxy-app-compat:1.5.5'
compile 'com.arello-mobile:moxy-android:1.5.5'
annotationProcessor 'com.arello-mobile:moxy-compiler:1.5.5'
MyFragmentView
#StateStrategyType(AddToEndSingleStrategy.class)
public interface MyFragmentView extends MvpView {
void showProgressDialog();
void hideProgressDialog();
void showTextTestMessage(String s);
void showCouldNotRetrieveAccounts();
}
MyFragmentPresenter
#PerFragment
#InjectViewState
public class MyFragmentPresenter extends MvpPresenter<MyFragmentView> {
private ApiService apiService;
#Inject
public MyFragmentPresenter(ApiService apiService) {
this.apiService = apiService;
}
public void getAccounts() {
getViewState().showProgressDialog();
getCompositeDisposable().add(apiService.getAccounts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(accountsResponse -> {
if (accountsResponse.err_code != 0) {
throw new LogicException(accountsResponse.message);
}
return accountsResponse;
})
.subscribe(
accountsResponse -> {
getViewState().setAccounts(accountsResponse);
getViewState().hideProgressDialog();
},
throwable -> {
getViewState().hideProgressDialog();
getViewState().showCouldNotRetrieveAccounts();
}
)
);
}
}
MyFragment
public class MyFragment extends MvpAppCompatFragment implements MyFragmentView {
#Inject
#InjectPresenter
public MyFragmentPresenter mPresenter;
#ProvidePresenter
public MyFragmentPresenter providePresenter() {
return mPresenter;
}
#Override
public void onActivityCreated (#Nullable Bundle savedInstanceState){
ComponentManager.getInstance().getActivityComponent(getActivity()).inject((MyActivity) getActivity());
super.onActivityCreated(savedInstanceState);
mPresenter.getAccounts();
}
#Override
public void showProgressDialog() {
// some code
}
#Override
public void hideProgressDialog() {
// some code
}
#Override
public void setAccounts(AccountsResponse accounts) {
// some code
}
#Override
public void showCouldNotRetrieveBankAccounts() {
// some code
}
}
**MyFragmentPagerAdapter **
public class MyFragmentPagerAdapter extends FragmentStatePagerAdapter {
#Override
public Fragment getItem (int position){
switch (position) {
case 0:
//
case 1:
//
case 2:
return new MyFragment();
}
return null;
}
#Override
public int getCount() {
return 3;
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}
I think something went wrong with connecting dagger with moxy.
Check the following issue on GitHub:
https://github.com/Arello-Mobile/Moxy/issues/100
Guys solve the same problem there
public class FilterFragmentExp extends BaseFragmentFilter implements ListingMVPview{
#Inject PresenterListing presenterExp;
private FilterUtilsModel filterModel;
private ListingModel listingModel,listingModel_tmp;
public static FilterFragmentExp newInstance(FilterUtilsModel filterModel,ListingModel listingModel) {
FilterFragmentExp fragment = new FilterFragmentExp();
Bundle bundle = new Bundle();
bundle.putSerializable("filter",filterModel);
bundle.putSerializable("listing",listingModel);
fragment.setArguments(bundle);
return fragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getComponent().inject(this);
presenterExp.attachView(this);
receiveArguments();
filterModel.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
#Override
public void onPropertyChanged(Observable observable, int i) {
presenterExp.initialize(filterModel.getExpMap(),TYPE_EXP);
}
});
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
#Override
public void onDestroy() {
super.onDestroy();
presenterExp.detachView();
}
#Override
protected int getLayout() {
return R.layout.fragment_filter_exp;
}
#Override
protected FilterUtilsModel getFilter() {
}
private void receiveArguments(){
filterModel = (FilterUtilsModel) getArguments().getSerializable("filter");
listingModel = (ListingModel) getArguments().getSerializable("listing");
listingModel_tmp = listingModel;
}
#Override
public void showLoading() {
}
#Override
public void hideLoading() {
}
#Override
public void showRetry() {
}
#Override
public void hideRetry() {
}
#Override
public void showError(String message) {
}
}
When I moved to this fragment for the first time everything works well, but from next time(pressed back and come back again) I am getting a nullpointer exception from this line -
presenterExp.initialize(filterModel.getExpMap(),TYPE_EXP);
I am supposed to get this error only if I wont set any view(MVP view, here its ListingMVPview) to the presenter object(presenterExp), but I already set it on this line
presenterExp.attachView(this);
Here is my Presenter -
public class PresenterListing extends BasePresenter<ListingMVPview> {
private final Context context;
#Inject
public PresenterListing(#ActivityContext Context context) {
this.context = context;
}
#Override
public void attachView(ListingMVPview mvpView) {
super.attachView(mvpView);
}
#Override
public void detachView() {
super.detachView();
}
public void initialize(Map options, String type) {
getMVPView().showLoading();
this.getListing(options,type);
}
....
}
Here in the above code getMVPView() returning null, though I have set MVP view.
I have used Dagger2 for dependency injection and using constructor injection for creating PresenterListing objects and using MVP architecture.
Any clues would be helpful as this problem is happening on when called from onPropertyChanged, if I move the code to some other place(say on some view's onclicklistener) everything works fine
you can add a null judgement
if (null) {...
advise start to use google mvp, good lucky
I am trying to develop an app using Mosby and EventBus. First event I want to have is after user login, creating a sticky event so every screen can access Login info at all times.
Based off the Mosby mail sample, I have a BasePresenter like this:
public abstract class EventBusPresenter<V extends MvpView> extends MvpBasePresenter<V> {
private static final java.lang.String TAG = tag(EventBusPresenter.class);
#Inject
protected EventBus mEventBus;
LoginSuccessfulEvent userInfo;
#Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onLoginSuccessful(LoginSuccessfulEvent event) {
MCLog.i(TAG, "Received a login event!");
userInfo = event;
onLoginReceived(event);
}
protected abstract void onLoginReceived(LoginSuccessfulEvent e);
public LoginSuccessfulEvent getUserInfo(){
return userInfo;
}
#Override
public void attachView(V view) {
super.attachView(view);
mEventBus.register(this);
}
#Override
public void detachView(boolean retainInstance) {
super.detachView(retainInstance);
mEventBus.unregister(this);
}
}
When the user logins I use this code:
public void doLogin(String username, String password) {
if (isViewAttached()) {
getView().showLoading();
}
cancelSubscription();
mSubscriber = new Subscriber<AuthenticationResponse>() {
#Override
public void onCompleted() {
if (isViewAttached()) {
getView().loginSuccessful();
}
}
#Override
public void onError(Throwable e) {
if (isViewAttached()) {
getView().showLoginError();
}
}
#Override
public void onNext(AuthenticationResponse authenticationResponse) {
User user = authenticationResponse.getUser();
MCLog.w(TAG, String.format("Login was successful with user: %s", user) );
mEventBus.postSticky(new LoginSuccessfulEvent(user, authenticationResponse.getToken()));
String token = authenticationResponse.getToken();
MCLog.i(TAG, String.format("Token obtained = %s", token));
mSharedPreferences.edit().putString(PreferenceKeys.TOKEN_KEY, token).apply();
}
};
My idea is that for every screen, as soon as its loaded it can retrieve the UserInfo through the EventBus subscription.
The issue is -- this event arrives too son. As per mosby's own BaseFragment class I do this:
public abstract class BaseFragment<V extends MvpView, P extends MvpPresenter<V>> extends MvpFragment<V, P> {
private Unbinder mUnbinder;
#LayoutRes
protected abstract int getLayoutRes();
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container,
#Nullable Bundle savedInstanceState) {
return inflater.inflate(getLayoutRes(), container, false);
}
#Override public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
inject();
super.onViewCreated(view, savedInstanceState);
mUnbinder = ButterKnife.bind(this, view);
}
#Override public void onDestroyView() {
super.onDestroyView();
mUnbinder.unbind();
}
/**
* Inject dependencies
*/
protected void inject() {
}
}
This means that injection arrives before the views are created, so whenever I try to respond to a LoginEvent being received, the UI elements that need updating are not there anymore.
How can I achieve this? Is this even the right way to do it?
Do mUnbinder = ButterKnife.bind(this, view); either in onCreateView() or if you prefer to do that in onViewCreated() the call mUnbinder = ButterKnife.bind(this, view); before calling super.onViewCreated().
Btw. Using an EventBus for that sounds painful. Why not simply having a class like UserMananger where you can get the current authenticated user or null if user is not authenticated. Also, since you are using RxJava it would make sense to include the UserManager in your RxJava chain / stream on each screen.
I am reinventing my app using a classic MVP approach. In order to to this I read many many articles and tutorials, and what I came out with is that the best way is to :
create an interface for the presenter and one for the view
make fragments and activities implements view interfaces
create an implementation of the presenter interface, which takes in the constructor an instance of the the view it manages, and hold a reference to the presenter inside the view's implementation.
So I have created this classes
VIEW INTERFACE
public interface SignupEmailView extends BaseView {
void fillEmail(String email);
void onEmailInvalid(String error);
void onDataValidated();
}
PRESENTER INTERFACE
public interface SignupEmailPresenter {
void initData(Bundle bundle);
void validateData(String email);
}
VIEW IMPLEMENTATION
public class FrSignup_email extends BaseSignupFragmentMVP implements IBackHandler, SignupEmailView {
public static String PARAM_EMAIL = "param_email";
#Bind(R.id.signup_step2_new_scrollview)
ScrollView mScrollview;
#Bind(R.id.signup_step2_new_lblTitle)
SuperLabel mLblTitle;
#Bind(R.id.signup_step2_new_lblSubtitle)
TextView mLblSubtitle;
#Bind(R.id.signup_step2_new_txtEmail)
EditText mTxtEmail;
#Bind(R.id.signup_step2_new_btnNext)
Button mBtnNext;
protected SignupActivityView mActivity;
SignupEmailPresenter mPresenter;
public FrSignup_email() {
// Required empty public constructor
}
public static FrSignup_email newInstance(String email) {
FrSignup_email fragment = new FrSignup_email();
Bundle b = new Bundle();
b.putString(PARAM_EMAIL, email);
fragment.setArguments(b);
return fragment;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mActivity = (SignupActivityView) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement IResetPasswordBridge");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = loadView(inflater, container, savedInstanceState, R.layout.fragment_signup_email);
mPresenter = new SignupEmailPresenterImpl(this);
ButterKnife.bind(this, view);
return view;
}
#Override
public final void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
applyCircularReveal();
mPresenter.initData(this.getArguments());
mTxtEmail.setImeOptions(EditorInfo.IME_ACTION_NEXT);
mTxtEmail.setOnEditorActionListener(new TextView.OnEditorActionListener() {
#Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_NEXT) {
mPresenter.validateData(mTxtEmail.getText().toString());
return true;
}
return false;
}
});
mTxtEmail.setOnTouchListener(new OnTouchCompoundDrawableListener_NEW(mTxtEmail, new OnTouchCompoundDrawableListener_NEW.OnTouchCompoundDrawable() {
#Override
public void onTouch() {
mTxtEmail.setText("");
}
}));
mBtnNext.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mPresenter.validateData(mTxtEmail.getText().toString());
}
});
}
#Override
public void fillEmail(String email) {
mTxtEmail.setText(email);
}
#Override
public void onEmailInvalid(String error) {
displayError(error);
}
#Override
public void onDataValidated() {
changeFieldToValid(mTxtEmail);
setEmail(mTxtEmail.getText().toString());
// the activity shows the next fragment
mActivity.onEmailValidated();
}
#Override
public boolean doBack() {
if (!isLoading()) {
mActivity.onEmailBack();
}
return true;
}
#Override
public void displayError(String error) {
changeFieldToInvalid(mTxtEmail);
mLblSubtitle.setText(error);
mLblSubtitle.setTextColor(ContextCompat.getColor(getActivity(), R.color.field_error));
}
}
PRESENTER IMPLEMENTATION
public class SignupEmailPresenterImpl implements SignupEmailPresenter {
private SignupEmailView mView;
public SignupEmailPresenterImpl(SignupEmailView view) {
mView = view;
}
#Override
public void initData(Bundle bundle) {
if (bundle != null) {
mView.fillEmail(bundle.getString(FrSignup_email.PARAM_EMAIL));
}
}
#Override
public void validateData(String password) {
ValidationUtils_NEW.EmailStatus status = ValidationUtils_NEW.validateEmail(password);
if (status != ValidationUtils_NEW.EmailStatus.VALID) {
mView.onEmailInvalid(ValidationUtils_NEW.getEmailErrorMessage(status));
} else {
mView.onDataValidated();
}
}
}
Now the fragment is held by an activity which implements this view interface and has its own presenter
public interface SignupActivityView extends BaseView {
void onEmailValidated();
void onPhoneNumberValidated();
void onPasswordValidated();
void onUnlockCodeValidated();
void onResendCodeClick();
void onEmailBack();
void onPhoneNumberBack();
void onPasswordBack();
void onConfirmCodeBack();
void onSignupRequestSuccess(boolean resendingCode);
void onSignupRequestFailed(String errorMessage);
void onTokenCreationFailed();
void onUnlockSuccess();
void onUnlockError(String errorMessage);
void showTermsAndConditions();
void hideTermsAndConditions();
}
My idea is to have a unit test for each project unit, so for each view and presenter implementation I want a unit test, so I want to unit test my fragment with roboletric, and for example I want to test that if I click the "NEXT" button and the email is correct, the hosting Activity's onEmailValidated()method is called. This is my test class
public class SignupEmailViewTest {
private SignupActivity_NEW mActivity;
private SignupActivity_NEW mSpyActivity;
private FrSignup_email mFragment;
private FrSignup_email mSpyFragment;
private Context mContext;
#Before
public void setUp() {
final Context context = RuntimeEnvironment.application.getApplicationContext();
this.mContext = context;
mActivity = Robolectric.buildActivity(SignupActivity_NEW.class).create().visible().get();
mSpyActivity = spy(mActivity);
mFragment = FrSignup_email.newInstance("");
mSpyFragment =spy(mFragment);
mSpyActivity.getFragmentManager()
.beginTransaction()
.replace(R.id.signupNew_fragmentHolder, mSpyFragment)
.commit();
mSpyActivity.getFragmentManager().executePendingTransactions();
}
#Test
public void testEmailValidation() {
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblTitle).isShown());
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle).isShown());
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
assertTrue(((SuperLabel) mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle)).getText().equals(mContext.getString(R.string.email_empty)));
((EditText) mSpyActivity.findViewById(R.id.signup_step2_new_txtEmail)).setText("aaa#bbb.ccc");
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
verify(mSpyFragment).onDataValidated();
verify(mSpyActivity).onEmailValidated();
}
}
everything works well, is just the last verify which doesn't work. Note that the previous verify works, so onEmailValidated is called for sure.
Aside from this specific case, I have some point to discuss:
If with roboeletric I am forced to use an activity to instantiate a fragment, how can I test the fragment in complete isolation (which would be the unit tests goal)? I mean, if I use Robolectric.setupActivity(MyActivity.class) and the activity instantiates somewhere a fragment, it will load the activity and the fragment, which is good, but what if the activity manages a flow of fragments? How can I test the second or third fragment without manually navigating to it? Someone can say to use a dummy activity and use FragmentTestUtil.startFragment, but what in the fragment's onAttach() method is implemented the bridging with the parent activity? Is it me going on the wrong way or are this problems still unsolved?
thanks
Actually you don't even require Roboelectric to do any of those tests.
If each fragment/activity implements a different view interface you could implement fake views and instantiate those instead of the activity/fragment. In this way you could have isolated tests.
If you don't want to implement all the methods of the view interface you could use Mockito and stub only the ones that your unit test requires.
Let me know if you need sample code.