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");
}
}