2016-07-11 2 views
10
Android Studio 2.1.2 

ich testen möchten, dass die Rückrufe onUsernameError, onPasswordError und onSuccess im LoginModelImp tatsächlich genannt. Ich bin mir nicht sicher, wie ich Event-Listener testen soll. Der Test schlägt jedoch fehl, da diese Funktionen nie aufgerufen werden. Ich verspotte sie mit Spott und versuche sie zu überprüfen.Unit Testing MVP mit Mockito mit Event-Listener

Dies ist mein Code so weit.

Presenter Schnittstelle

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

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

Presenter Implementierung

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

Model-Schnittstelle

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

Modell Implementation

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

Gibt es eine Möglichkeit, diese Umsetzung zu testen?

Vielen Dank für Ihre Anregungen,

Antwort

4

Die Testklasse LoginPresenterImpTest über den Test von LoginPresenterImp Klasse ist, und es sollte nur die tatsächliche Umsetzung und die Mocks ihrer Mitarbeiter nutzen. Die Klasse LoginModelContract.OnLoginCompletedListener ist ein Kollaborateur von , also in einem gut entworfenen und reinen Unit-Test von LoginPresenterImp, wie deiner, ist es vollkommen normal, dass er niemals aufgerufen wird. Die Lösung, die ich vorschlage, ist die LoginModelImp separat zu testen:

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

Alternativ müssen Sie die tatsächliche Umsetzung der LoginModelImp in Ihrem LoginPresenterImpTest und Spion auf Ihrem Hörer verwenden (das ist der Moderator selbst) oder die Mocks konfigurieren damit sie den Hörer anrufen.Hier ist ein Beispiel, aber ich würde nicht verwenden diese:

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

danke, Das scheint eine gute Lösung zu sein. Ich warte nur darauf, andere Antworten zu sehen. – ant2009

+0

Hey @Lorenzo Ich habe das gleiche Problem, aber in meinem Fall möchte ich gegen SQLite-Datenbank jede Idee validieren, wie man das macht? Danke – Tony

0

ich Ihren Punkt fehlt könnte, aber haben Sie versucht PowerMock mit?

Hier finden Sie die folgenden Abhängigkeiten müssen:

  • testCompile "org.powermock: PowerMock-Modul-junit4: 1.6.5"
  • testCompile „org.powermock: PowerMock-Modul-junit4-Regel : 1.6.5"
  • testCompile "org.powermock: PowerMock-api-Mockito: 1.6.5"
  • testCompile "org.powermock: PowerMock-classloading-xstream: 1.6.5"

Und dann verwenden Sie es auf diese Weise:

@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

Ich hatte ein Problem mit PowerMockito.spy (new DownloadPresenterContract.Events()) Da Sie keine neue Instanz einer Schnittstelle erstellen können. Allerdings habe ich den Code in meiner Frage zu etwas leichter zu verstehen geändert. Vielen Dank. – ant2009

+0

Sie müssen keine Instanz mit PowerMockito.spy (new DownloadPresenterContract.Events()) erstellen. Sie können folgendermaßen vorgehen: PowerMockito.whenNew (DownloadPresenterContract.Events.class) .withNoArguments(). ThenReturn (Mockito.mock (DownloadPresenterContract.Events.class)). Mit anderen Worten, du sagst es "egal, wer eine neue Instanz von diesem erstellen wird, gib ihm eine verspottete Antwort". –

+0

In jedem Fall gibt es zwei Möglichkeiten: 1) Sie anwenden Classloader Magie (wie PowerMock) machen AST Proxies um Klassen; 2) Sie wenden OOP-Magie an, die jeden verspottet, bis Sie bekommen, was Sie brauchen. Ich bevorzuge die erste Variante, weil sie kürzer ist. Auch wenn es sich um eine Schnittstelle handelt, sollte es jemanden geben, der eine Konstruktorreferenz aufruft. –

1

Ich denke, weil Sie die LoginModelContract verspotten und OnLoginCompletedListener man kann nicht behaupten, dass onUsernameError, onPasswordError und onSuccess werden, weil tatsächlich genannt durch LoginModelContract spöttisch die " echte "Login-Methode (die diese Methoden aufrufen sollte) würde nicht ausgeführt werden, sondern nur die verspottete Methode würde aufgerufen werden. Sie könnten diese Methoden mit so etwas wie auslösen:

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

Aber Ursache solcher Test wäre nicht sinnvoll, weil Sie explizit fordern, was Sie testen wollen.

Meiner Meinung nach wäre es sinnvoll, nur die LoginModelContract zu testen, ohne die LoginFragmentViewContract und LoginPresenterContract. Etwas wie:

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

Dies läuft darauf hinaus, den Unterschied zwischen User Story und Use Case unten. In diesem Fall haben Sie 1 User Story (zB "Als Benutzer möchte ich mich anmelden, also gebe ich meinen Benutzernamen und mein Passwort ein"), aber es gibt tatsächlich mindestens 3 Use Cases: Richtiger Benutzername/Richtiges Passwort, Richtiger Benutzername/Kennwort falsch, falsch Benutzername/Passwort Recht usw. Als allgemeines best Practice, mögen Sie für Tests 1 entsprechen: 1 mit Use Cases, also würde ich so etwas wie dies empfehlen:

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

mit anderen Worten, für Test 1, Sie versuchen, positiv zu überprüfen, dass, wenn der Benutzername und das Passwort abgeschlossen sind, Success aufgerufen wird. Für Test 2 überprüfen Sie die Bedingungen für den Aufruf von UserNameError und für 3 die für onPasswordError. Alle drei sind gültige Dinge zu testen, und Sie haben Recht zu überprüfen möchten, dass sie aufgerufen werden, aber Sie müssen sie als unterschiedliche Use Cases behandeln.

Der Vollständigkeit halber würde ich überprüfen, was bei Wrong_User/Wrong_Password passiert, und auch überprüfen, was passiert, wenn es eine Wrong_Password-Bedingung N-mal gibt (brauchen Sie die Sperrung des Kontos?).

Hoffe, das hilft. Viel Glück.