Unit Testing MVP с использованием mockito с прослушивателями событий

Android Studio 2.1.2

Я хочу проверить, действительно ли вызываются обратные вызовы onUsernameError, onPasswordError и onSuccess в LoginModelImp. Я не уверен, как тестировать слушателей событий. Тем не менее, тест терпит неудачу, поскольку эти функции никогда не вызываются. Я издеваюсь над ними с помощью mockito и пытаюсь проверить их.

Это мой код до сих пор.

Интерфейс презентатора

public interface LoginPresenterContract<LoginFragmentViewContract> {
    void validateCredentials();

    void attachView(LoginFragmentViewContract view);
    void detachView();
}

Реализация презентатора

public class LoginPresenterImp implements LoginPresenterContract<LoginFragmentViewContract>, LoginModelContract.OnLoginCompletedListener {

    private LoginModelContract mLoginModelContract;
    private LoginFragmentViewContract mLoginFragmentView;

    public LoginPresenterImp(LoginModelContract loginModelContract) {
        mLoginModelContract = loginModelContract;
    }

    /*
     * LoginPresenterContact - implementation
     */
    @Override
    public void attachView(LoginFragmentViewContract view) {
        mLoginFragmentView = view;
    }

    @Override
    public void detachView() {
        mLoginFragmentView = null;
    }

    @Override
    public void validateCredentials() {
        if(mLoginModelContract != null) {
            mLoginModelContract.login(
                    mLoginFragmentView.getUsername(),
                    mLoginFragmentView.getPassword(),
                    LoginPresenterImp.this);
        }
    }

    /*
     * LoginModelContract.OnLoginCompletedListener - implementation
     */
    @Override
    public void onUsernameError() {
        if(mLoginFragmentView != null) {
            mLoginFragmentView.onLoginFailed("Incorrect username");
        }
    }

    @Override
    public void onPasswordError() {
        if(mLoginFragmentView != null) {
            mLoginFragmentView.onLoginFailed("Incorrect password");
        }
    }

    @Override
    public void onSuccess() {
        if(mLoginFragmentView != null) {
            mLoginFragmentView.onLoginSuccess();
        }
    }
}

Интерфейс модели

public interface LoginModelContract {
    interface OnLoginCompletedListener {
        void onUsernameError();
        void onPasswordError();
        void onSuccess();
    }
    void login(String username, String password, OnLoginCompletedListener onLoginCompletedListener);
}

Реализация модели

public class LoginModelImp implements LoginModelContract {
    /* Testing Valid username and passwords */
    private static String validUsername = "steve";
    private static String validPassword = "1234";

    @Override
    public void login(final String username,
                      final String password,
                      final OnLoginCompletedListener onLoginCompletedListener) {

        boolean hasSuccess = true;
        if(TextUtils.isEmpty(username) || !username.equals(validUsername)) {
        /* TEST onUsernameError() */
            onLoginCompletedListener.onUsernameError();
            hasSuccess = false;
        }

        if(TextUtils.isEmpty(password) || !password.equals(validPassword)) {
        /* TEST onPasswordError() */
            onLoginCompletedListener.onPasswordError();
            hasSuccess = false;
        }

        if(hasSuccess) {
        /* TEST onSuccess() */
            onLoginCompletedListener.onSuccess();
        }
    }
}

Тест JUnit4 с Mockito

public class LoginPresenterImpTest {
    private LoginFragmentViewContract mMockViewContract;
    private LoginModelContract mMockModelContract;
    private LoginModelContract.OnLoginCompletedListener mMockOnLoginCompletedListener;
    private LoginPresenterContract<LoginFragmentViewContract> mLoginPresenterContract;

    @Before
    public void setUp() throws Exception {
        mMockViewContract = Mockito.mock(LoginFragmentViewContract.class);
        mMockModelContract = Mockito.mock(LoginModelContract.class);
        mMockOnLoginCompletedListener = Mockito.mock(LoginModelContract.OnLoginCompletedListener.class);
        mLoginPresenterContract = new LoginPresenterImp(mMockModelContract);
        mLoginPresenterContract.attachView(mMockViewContract);
    }

    @Test
    public void shouldSuccessWithValidCredentials() {
        when(mMockViewContract.getUsername()).thenReturn("steve");
        when(mMockViewContract.getPassword()).thenReturn("1234");

        mLoginPresenterContract.validateCredentials();

        verify(mMockViewContract, times(1)).getUsername();
        verify(mMockViewContract, times(1)).getPassword();

        verify(mMockOnLoginCompletedListener, times(1)).onSuccess();

        verify(mMockOnLoginCompletedListener, never()).onPasswordError();
        verify(mMockOnLoginCompletedListener, never()).onUsernameError();
    }
}

Есть ли способ протестировать эту реализацию?

Большое спасибо за любые предложения,

Ответ 1

Тест-класс LoginPresenterImpTest посвящен тестированию класса LoginPresenterImp, и он должен использовать только свою фактическую реализацию и издевательства соавторов. Класс LoginModelContract.OnLoginCompletedListener является сотрудником LoginModelImp, поэтому в хорошо продуманном и чистом модульном тесте LoginPresenterImp, как и у вас, совершенно нормально, что он никогда не вызывается. Решение, которое я предлагаю, заключается в том, чтобы отдельно протестировать LoginModelImp:

public class LoginModelImpTest {

    private LoginModelContract.OnLoginCompletedListener mMockOnLoginCompletedListener;
    private LoginModelImp loginModelImp;

    @Before
    public void setUp() throws Exception {
        mMockOnLoginCompletedListener = Mockito.mock(LoginModelContract.OnLoginCompletedListener.class);
        loginModelImp = new LoginModelImp();
    }

    @Test
    public void shouldSuccessWithValidCredentials() {

        loginModelImp.login("steve", "1234", mMockOnLoginCompletedListener);;

        verify(mMockOnLoginCompletedListener, times(1)).onSuccess();

        verify(mMockOnLoginCompletedListener, never()).onPasswordError();
        verify(mMockOnLoginCompletedListener, never()).onUsernameError();
    }
}

В качестве альтернативы вам нужно использовать фактическую реализацию LoginModelImp в LoginPresenterImpTest и шпионить на вашем слушателе (то есть сам ведущий) или настроить макеты, чтобы заставить их вызвать слушателя. Вот пример, но я бы не использовал этот:

public class LoginPresenterImpTest {
    private LoginFragmentViewContract mMockViewContract;
    private LoginModelContract mModelContract;
    private LoginModelContract.OnLoginCompletedListener mMockOnLoginCompletedListener;
    private LoginPresenterContract<LoginFragmentViewContract> mLoginPresenterContract;

    @Before
    public void setUp() throws Exception {
        mMockViewContract = Mockito.mock(LoginFragmentViewContract.class);
        mModelContract = new LoginModelImp();
        LoginPresenterImp spyPresenterImp = Mockito.spy(new LoginPresenterImp(mModelContract));
        mLoginPresenterContract = spyPresenterImp;
        mMockOnLoginCompletedListener = spyPresenterImp;
        mLoginPresenterContract.attachView(mMockViewContract);
    }

    @Test
    public void shouldSuccessWithValidCredentials() {
        when(mMockViewContract.getUsername()).thenReturn("steve");
        when(mMockViewContract.getPassword()).thenReturn("1234");

        mLoginPresenterContract.validateCredentials();

        verify(mMockViewContract, times(1)).getUsername();
        verify(mMockViewContract, times(1)).getPassword();

        verify(mMockOnLoginCompletedListener, times(1)).onSuccess();

        verify(mMockOnLoginCompletedListener, never()).onPasswordError();
        verify(mMockOnLoginCompletedListener, never()).onUsernameError();
    }
}

Ответ 2

Это сводится к различию между Историей пользователей и Вариантом использования. В этом случае у вас есть 1 пользовательская история (например, "Как пользователь, я хочу войти в систему, поэтому я предоставляю свое имя пользователя и пароль" ), но на самом деле есть как минимум 3 случая использования: правый пароль пользователя/правый пароль, Неверный пароль, неправильный пароль пользователя/пароль и т.д. Как общая рекомендация, вы хотите, чтобы тесты соответствовали 1:1 с использованием случаев использования, поэтому я бы рекомендовал что-то вроде этого:

 @Test
 public void shouldCompleteWithValidCredentials() {
    mMockModelContract.login("steve", "1234", 
                              mMockOnLoginCompletedListener);

    verify(mMockOnLoginCompletedListener, times(1)).onSuccess();     
 }

 @Test
 public void shouldNotCompleteWithInvalidUser() {
    mMockModelContract.login("wrong_user", "1234",
                               mMockOnLoginCompletedListener);
    verify(mMockOnLoginCompletedListener, 
                            times(1)).onUsernameError();      
 }

@Test
public void shouldNotCompleteWithInvalidPassword() {
    mMockModelContract.login("steve", "wrong_password", 
                         mMockOnLoginCompletedListener);
    verify(mMockOnLoginCompletedListener, times(1)).onPasswordError();
}

Другими словами, для теста 1 вы пытаетесь положительно проверить, что, когда имя пользователя и пароль завершены, вызывается Success. Для теста 2 вы проверяете условия для вызова onUsernameError и для 3, для onPasswordError. Все три являются действительными для тестирования, и вы правы, чтобы проверить, что они вызваны, но вам нужно рассматривать их как разные Случаи использования.

Для полноты я проверил бы, что происходит на Wrong_User/Wrong_Password, а также проверит, что произойдет, если есть условие Wrong_Password N раз (вам нужно блокировать учетную запись?).

Надеюсь, это поможет. Удачи.

Ответ 3

Я думаю, потому что вы издеваетесь над LoginModelContract и OnLoginCompletedListener, вы не можете утверждать, что onUsernameError, onPasswordError и onSuccess на самом деле вызываются, потому что, издеваясь LoginModelContract, "реальный" метод входа ( который должен вызывать эти методы) не будет выполнен, но будет вызван только издевательский метод. Вы можете вызвать эти методы с помощью чего-то вроде:

Mockito.doAnswer(new Answer<Void>() {
    @Override
    public Void answer(InvocationOnMock invocation) throws Throwable {
        Object[] args = invocation.getArguments();
        OnLoginCompletedListener listener = (OnLoginCompletedListener) args[2];
        listener.onUsernameError();
        return null;
    }
}).when(mMockModelContract).login(anyString(), anyString(), any(OnLoginCompletedListener.class)).thenAnswer();

Но причина такого теста не будет иметь смысла, потому что вы явно вызываете то, что пытаетесь проверить.

По-моему, было бы разумнее просто проверить LoginModelContract без LoginFragmentViewContract и LoginPresenterContract. Что-то вроде:

public class LoginPresenterImpTest {
    private LoginModelContract mMockModelContract;
    private LoginModelContract.OnLoginCompletedListener mMockOnLoginCompletedListener;

    @Before
    public void setUp() throws Exception {
        mMockOnLoginCompletedListener = Mockito.mock(LoginModelContract.OnLoginCompletedListener.class);
        mMockModelContract = new LoginModelContract();
    }

    @Test
    public void shouldSuccessWithValidCredentials() {
        mMockModelContract.login("steve", "1234", mMockOnLoginCompletedListener);

        verify(mMockOnLoginCompletedListener, times(1)).onSuccess();

        verify(mMockOnLoginCompletedListener, never()).onPasswordError();
        verify(mMockOnLoginCompletedListener, never()).onUsernameError();
    }
}

Ответ 4

Мне не хватало вашей точки, но вы пробовали использовать PowerMock?

Вам понадобятся следующие зависимости:

  • testCompile "org.powermock: powermock-module-junit4: 1.6.5"
  • testCompile "org.powermock: powermock-module-junit4-rule: 1.6.5"
  • testCompile "org.powermock: powermock-api-mockito: 1.6.5"
  • testCompile "org.powermock: powermock-classloading-xstream: 1.6.5"

И затем используйте его следующим образом:

@PowerMockIgnore({ "org.mockito.*", "android.*" })
@PrepareForTest(DownloadPresenterContract.Events.class)
public class DownloadModelTest {

    @Rule
    public PowerMockRule rule = new PowerMockRule();

    private DownloadPresenterContract.Events mockEvents;

    @Before
    public void setUp() throws Exception {
         this.mockEvents = PowerMockito.spy(new DownloadPresenterContract.Events());

         PowerMockito.whenNew(DownloadPresenterContract.Events.class)
                     .withNoArguments()
                     .thenReturn(this.mockEvents); 
    }

    @Test
    public void testStaticMocking() {

         //Do your logic, which should trigger mockEvents actions

         Mockito.verify(this.mockEvents, Mockito.times(1)).onDownloadSuccess();
         //Or use this:
         //PowerMockito.verifyPrivate(this.mockEvents, times(1)).invoke("onDownloadSuccess", "someParam");
}

}