2016-07-11 21 views
10
Android Studio 2.1.2 

voglio provare che i callback onUsernameError, onPasswordError e onSuccess, nel LoginModelImp sono effettivamente chiamati. Non sono sicuro di come testare gli ascoltatori di eventi. Tuttavia, il test fallisce poiché tali funzioni non vengono mai chiamate. Li sto prendendo in giro con il mockito e sto provando a verificarli.Unit Testing MVP utilizzando Mockito con gli ascoltatori di eventi

Questo è il mio codice finora.

interfaccia Presentatore

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

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

implementazione Presentatore

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

interfaccia Modello

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

Attuazione Modello

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

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

Esiste un modo per testare questa implementazione?

Molte grazie per qualsiasi suggerimento,

risposta

4

classe Il test LoginPresenterImpTest è circa la prova del LoginPresenterImp di classe, e dovrebbe utilizzare solo la sua effettiva attuazione e le prende in giro dei suoi collaboratori. La classe LoginModelContract.OnLoginCompletedListener è un collaboratore di LoginModelImp, quindi in un test unitario ben progettato e puro di LoginPresenterImp, come il tuo, è perfettamente normale che non venga mai chiamato. La soluzione che propongo è quello di testare la LoginModelImp separatamente:

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

In alternativa, è necessario utilizzare l'effettiva attuazione di LoginModelImp nella vostra spia LoginPresenterImpTest e sul vostro ascoltatore (cioè il presentatore stesso) o per configurare le prende in giro per farli chiamare l'ascoltatore.Ecco un esempio, ma non vorrei utilizzare questo:

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

grazie, sembra una buona soluzione. Sto solo aspettando di vedere altre risposte. – ant2009

+0

Ehi @Lorenzo sto affrontando lo stesso problema ma nel mio caso voglio convalidare contro la base dati sqlite qualsiasi idea su come farlo? Grazie – Tony

0

potevo mancare il vostro punto, ma hai provato ad utilizzare PowerMock?

Hai bisogno dei seguenti dipendenze:

  • testCompile "org.powermock: powermock-modulo-Junit4: 1.6.5"
  • testCompile "org.powermock: powermock-modulo-Junit4-regola : 1.6.5"
  • testCompile "org.powermock: powermock-api-Mockito: 1.6.5"
  • testCompile "org.powermock: powermock-classloading-xstream: 1.6.5"

e quindi utilizzarlo in questo modo:

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

}

+0

Ho riscontrato un problema con PowerMockito.spy (nuovo DownloadPresenterContract.Events()) Poiché non è possibile creare una nuova istanza di un'interfaccia. Tuttavia, ho cambiato il codice nella mia domanda a qualcosa di più facile da capire. Grazie. – ant2009

+0

Non è necessario creare un'istanza utilizzando PowerMockito.spy (new DownloadPresenterContract.Events()). Puoi farlo in questo modo: PowerMockito.whenNew (DownloadPresenterContract.Events.class) .withNoArguments(). ThenReturn (Mockito.mock (DownloadPresenterContract.Events.class)). In altre parole si dice "non importa chi creerà una nuova istanza di questo, dargli una risposta derisoria". –

+0

In ogni caso ci sono due opzioni: 1) stai applicando magic classloader (come PowerMock) rendendo i proxy AST attorno alle classi; 2) stai applicando la magia di OOP a tutti coloro che ti prendono ciò che ti serve. Preferisco la prima variante perché è più corta. Anche se hai a che fare con un'interfaccia, dovrebbe esserci qualcuno concreto che sta chiamando un riferimento costruttore. –

1

credo perché si sta prendendo in giro il LoginModelContract e OnLoginCompletedListener non si può affermare che onUsernameError, onPasswordError, e onSuccess sono effettivamente chiamati perché deridendo LoginModelContract della " il vero "metodo di login" (che dovrebbe chiamare questi metodi) non verrebbe eseguito, ma sarebbe chiamato solo il metodo deriso. Si potrebbe innescare questi metodi con qualcosa di simile:

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

Ma di causa un simile test renderebbe non senso, perché si sta chiamando esplicitamente ciò che si sta cercando di verificare.

A mio parere, sarebbe più sensato testare semplicemente lo LoginModelContract senza il LoginFragmentViewContract e LoginPresenterContract. Qualcosa di simile:

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

Questo riduce alla differenza tra User Story e Use Case. In questo caso, hai 1 User Story (es. "Come utente, voglio effettuare il login, quindi fornisco il mio nome utente e password"), ma in realtà ci sono almeno 3 casi d'uso: diritto utente/password corretta, nome utente giusto/password sbagliato, sbagliato nome utente/password corretta, ecc come procedura generale, si vuole per le prove a corrispondono 1: 1 con casi d'uso, quindi vi consiglio qualcosa di simile:

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

In altre parole, per Test 1, si sta tentando di verificare in modo positivo che, quando il nome utente e la password sono completi, viene richiamato il successo. Per Test 2, stai verificando le condizioni per chiamare su UserName e per 3, quelle per OnPasswordError. Tutti e tre sono argomenti validi da testare e hai ragione a voler verificare che vengano chiamati, ma devi trattarli come casi d'uso diversi.

Per completezza, vorrei verificare cosa succede su Wrong_User/Wrong_Password, e anche verificare cosa succede se c'è una condizione Wrong_Password N volte (hai bisogno di bloccare l'Account?).

Spero che questo aiuti. In bocca al lupo.