Search code examples
androidunit-testingmockitoandroid-testingandroid-mvp

Unittesting when using Android Annotations, Mockito and MVP pattern


I've up until yesterday successfully put together a very readable Android project using the MVP-pattern and the Android Annotations library.

But yesterday when I started writing unittest for my LoginPresenter a problem has shown itself.

First some code from my LoginPresenter.

...
@EBean
public class LoginPresenterImpl implements LoginPresenter, LoginInteractor.OnLoginFinishedListener {

  @RootContext
  protected LoginActivity loginView;

  @Bean(LoginInteractorImpl.class)
  LoginInteractor loginInteractor;

  @Override public void validateCredentials(String username, String password) {
    if (loginView != null) {
        loginView.showProgress();
    }

    if (TextUtils.isEmpty(username)) {
        // Check that username isn't empty
        onUsernameError();
    }
    if (TextUtils.isEmpty(password)){
        // Check that password isn't empty
        onPasswordError();
        // No reason to continue to do login
    } else {

    }
}

  @UiThread(propagation = UiThread.Propagation.REUSE)
  @Override public void onUsernameError() {
    if (loginView != null) {
        loginView.setUsernameError();
        loginView.hideProgress();
    }
}

...

My test:

@RunWith(MockitoJUnitRunner.class)
public class LoginPresenterImplTest {

  private LoginPresenter loginPresenter;

  @Mock
  private LoginPresenter.View loginView;

  @Before
  public void setUp() {
      // mock or create a Context object
      Context context = new MockContext();

      loginPresenter = LoginPresenterImpl_.getInstance_(context);

      MockitoAnnotations.initMocks(this);
  }

  @After
  public void tearDown() throws Exception {
      loginPresenter = null;

  }

  @Test
  public void whenUserNameIsEmptyShowUsernameError() throws Exception {
      loginPresenter.validateCredentials("", "testtest");

      // verify(loginPresenter).onUsernameError();
      verify(loginView).setUsernameError();
  }
}

The problem is I've not used the standard approach of using MVP-pattern but instead trying out Android Annotations to make the code more readable. So I've not used attachView()- or detachView()-methods for attaching my presenter to my LoginActivity (view). This means that I can't mock my "view". Does someone know a workaround for this problem. I keep getting following message when running the test:

Wanted but not invoked:
loginView.setUsernameError();
-> at      com.conhea.smartgfr.login.LoginPresenterImplTest.whenUserNameIsEmptyShowUsernameError(LoginPresenterImplTest.java:48)
Actually, there were zero interactions with this mock.

Solution (I'm not using @RootContext anymore):

Presenter:

@EBean
public class LoginPresenterImpl extends AbstractPresenter<LoginPresenter.View>
        implements LoginPresenter, LoginInteractor.OnLoginFinishedListener {

    private static final String TAG = LoginPresenterImpl.class.getSimpleName();

    @StringRes(R.string.activity_login_authenticating)
    String mAuthenticatingString;

    @StringRes(R.string.activity_login_aborting)
    String mAbortingString;

    @StringRes(R.string.activity_login_invalid_login)
    String mInvalidCredentialsString;

    @StringRes(R.string.activity_login_aborted)
    String mAbortedString;

    @Inject
    LoginInteractor mLoginInteractor;

    @Override
    protected void initializeDagger() {
        Log.d(TAG, "Initializing Dagger injection");
        Log.d(TAG, "Application is :" + getApp().getClass().getSimpleName());
        Log.d(TAG, "Component is: " + getApp().getComponent().getClass().getSimpleName());
        Log.d(TAG, "UserRepo is: " + getApp().getComponent().userRepository().toString());
        mLoginInteractor = getApp().getComponent().loginInteractor();
        Log.d(TAG, "LoginInteractor is: " + mLoginInteractor.getClass().getSimpleName());
    }

    @Override
    public void validateCredentials(String username, String password) {
        boolean error = false;
        if (!isConnected()) {
            noNetworkFailure();
            error = true;
        }
        if (TextUtils.isEmpty(username.trim())) {
            // Check that username isn't empty
            onUsernameError();
            error = true;
        }
        if (TextUtils.isEmpty(password.trim())) {
            // Check that password isn't empty
            onPasswordError();
            error = true;
        }
        if (!error) {
            getView().showProgress(mAuthenticatingString);
            mLoginInteractor.login(username, password, this);
        }
    }
...

My tests (some of them):

@RunWith(AppRobolectricRunner.class)
@Config(constants = BuildConfig.class)
public class LoginPresenterImplTest {

@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();

private LoginPresenterImpl_ mLoginPresenter;

@Mock
private LoginPresenter.View mLoginViewMock;

@Mock
private LoginInteractor mLoginInteractorMock;

@Captor
private ArgumentCaptor<LoginInteractor.OnLoginFinishedListener> mCaptor;

@Before
public void setUp() {

    mLoginPresenter = LoginPresenterImpl_.getInstance_(RuntimeEnvironment.application);
    mLoginPresenter.attachView(mLoginViewMock);
    mLoginPresenter.mLoginInteractor = mLoginInteractorMock;
}

@After
public void tearDown() throws Exception {
    mLoginPresenter.detachView();
    mLoginPresenter = null;

}

@Test
public void whenUsernameAndPasswordIsValid_shouldLogin() throws Exception {

    String authToken = "Success";

    mLoginPresenter.validateCredentials("test", "testtest");

    verify(mLoginInteractorMock, times(1)).login(
            anyString(),
            anyString(),
            mCaptor.capture());
    mCaptor.getValue().onSuccess(authToken);
    verify(mLoginViewMock, times(1)).loginSuccess(authToken);
    verify(mLoginViewMock, times(1)).hideProgress();
}

@Test
public void whenUsernameIsEmpty_shouldShowUsernameError() throws Exception {

    mLoginPresenter.validateCredentials("", "testtest");


    verify(mLoginViewMock, times(1)).setUsernameError();
    verify(mLoginViewMock, never()).setPasswordError();
    verify(mLoginViewMock, never()).hideProgress();
}
...

Solution

  • As a workaround you can have this:

    public class LoginPresenterImpl ... {
    
        ...
    
        @VisibleForTesting
        public void setLoginPresenter(LoginPresenter.View loginView) {
            this.loginView = loginView;
        }
    
    }
    

    In test class:

    @Before
    public void setUp() {
        ...
    
        MockitoAnnotations.initMocks(this);
        loginPresenter.setLoginPresenter(loginView);
    }
    

    But, as a rule of thumb, when you see @VisibleForTesting annotation, that means you have ill architecture. Better to refactor your project.