10 votes

Test unitaire de MVP à l'aide de mockito et d'écouteurs d'événements

Android Studio 2.1.2

Je veux tester que les callbacks onUsernameError, onPasswordError, et onSuccess, dans le LoginModelImp sont effectivement appelés. Je ne sais pas exactement comment tester les auditeurs d'événements. Cependant, le test échoue car ces fonctions ne sont jamais appelées. Je les simule avec mockito et j'essaie de les vérifier.

Voici mon code jusqu'à présent.

Interface du présentateur

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

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

Mise en œuvre du présentateur

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

Modèle d'interface

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

Mise en œuvre du modèle

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

Test JUnit4 avec 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();
    }
}

Existe-t-il un moyen de tester cette mise en œuvre ?

Merci de me faire part de vos suggestions,

4voto

Lorenzo Murrocu Points 705

La classe de test LoginPresenterImpTest concerne le test de LoginPresenterImp et ne doit utiliser que son implémentation réelle et les mocks de ses collaborateurs. La classe LoginModelContract.OnLoginCompletedListener est un collaborateur de LoginModelImp Ainsi, dans un test unitaire bien conçu et pur de LoginPresenterImp Il est tout à fait normal qu'il ne soit jamais appelé. La solution que je propose est de tester le LoginModelImp séparément :

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

Sinon, vous devez utiliser l'implémentation réelle de LoginModelImp dans votre LoginPresenterImpTest et d'espionner votre auditeur (c'est-à-dire le présentateur lui-même) ou de configurer les mocks pour qu'ils appellent l'auditeur. Voici un exemple, mais je ne l'utiliserais pas :

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

2voto

Milton Hernandez Points 594

Cela se résume à la différence entre le récit de l'utilisateur et le cas d'utilisation. Dans ce cas, vous avez une histoire d'utilisateur (par exemple, "En tant qu'utilisateur, je veux me connecter, donc je fournis mon nom d'utilisateur et mon mot de passe"), mais il y a en fait au moins trois cas d'utilisation : Bon nom d'utilisateur/bon mot de passe, bon nom d'utilisateur/mauvais mot de passe, mauvais nom d'utilisateur/bon mot de passe, etc. En règle générale, il est préférable que les tests correspondent à 1:1 avec les cas d'utilisation, et je recommanderais donc quelque chose comme ceci :

 @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();
}

En d'autres termes, pour le test 1, vous essayez de vérifier positivement que, lorsque le nom d'utilisateur et le mot de passe sont complétés, le succès est invoqué. Pour le test 2, vous vérifiez les conditions d'appel de onUsernameError et pour le test 3, celles de onPasswordError. Ces trois tests sont valables et vous avez raison de vouloir vérifier qu'ils sont appelés, mais vous devez les traiter comme des cas d'utilisation différents.

Pour être complet, je vérifierais ce qui se passe en cas de Wrong_User/Wrong_Password, et également ce qui se passe si la condition Wrong_Password est remplie N fois (avez-vous besoin de bloquer le compte ?).

J'espère que cela vous aidera. Je vous souhaite bonne chance.

1voto

LazerBass Points 1015

Je pense que c'est parce que vous vous moquez de la LoginModelContract y OnLoginCompletedListener vous ne pouvez pas affirmer que onUsernameError , onPasswordError et onSuccess sont en fait appelées parce qu'en se moquant de LoginModelContract la "vraie" méthode de connexion (qui devrait appeler ces méthodes) ne serait pas exécutée, mais seule la méthode simulée serait appelée. Vous pourriez déclencher ces méthodes avec quelque chose comme :

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();

Mais bien sûr, un tel test n'aurait aucun sens car vous appelez explicitement ce que vous essayez de tester.

À mon avis, il serait plus judicieux de tester simplement la fonction LoginModelContract sans le LoginFragmentViewContract y LoginPresenterContract . Quelque chose comme :

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

0voto

Michael Cheremuhin Points 1136

Je n'ai peut-être pas compris votre point de vue, mais avez-vous essayé d'utiliser PowerMock ?

Vous aurez besoin des dépendances suivantes :

  • 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"

Puis utilisez-le de cette façon :

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

}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X