Android 使用mockito和事件监听器对MVP进行单元测试

Android 使用mockito和事件监听器对MVP进行单元测试,android,unit-testing,mockito,junit4,Android,Unit Testing,Mockito,Junit4,我想测试LoginModelImp中onUsernameError、onPasswordError和onSuccess的回调是否被实际调用。我不知道如何测试事件侦听器。但是,测试失败,因为这些函数从未被调用。我正在用mockito模仿他们,并试图验证他们 这是到目前为止我的代码 演示者界面 Android Studio 2.1.2 public class LoginPresenterImp implements LoginPresenterContract<LoginFragmentV

我想测试LoginModelImp中onUsernameError、onPasswordError和onSuccess的回调是否被实际调用。我不知道如何测试事件侦听器。但是,测试失败,因为这些函数从未被调用。我正在用mockito模仿他们,并试图验证他们

这是到目前为止我的代码

演示者界面

Android Studio 2.1.2
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 LoginPresenterContract<LoginFragmentViewContract> {
    void validateCredentials();

    void attachView(LoginFragmentViewContract view);
    void detachView();
}
public interface LoginModelContract {
    interface OnLoginCompletedListener {
        void onUsernameError();
        void onPasswordError();
        void onSuccess();
    }
    void login(String username, String password, OnLoginCompletedListener onLoginCompletedListener);
}
使用Mockito进行JUnit4测试

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();
        }
    }
}
public类loginpresentremotest{
private LoginFragmentViewContract mMockViewContract;
私有LoginModelContract mMockModelContract;
私有LoginModelContract.OnLoginCompletedListener mMockOnLoginCompletedListener;
私人LoginResenterContract MLLoginResenterContract;
@以前
public void setUp()引发异常{
mMockViewContract=Mockito.mock(LoginFragmentViewContract.class);
mMockModelContract=Mockito.mock(LoginModelContract.class);
mMockOnLoginCompletedListener=Mockito.mock(LoginModelContract.OnLoginCompletedListener.class);
mLoginPresenterContract=新的LoginPresenterImp(mMockModelContract);
mloginpresentract.attachView(mMockViewContract);
}
@试验
public void应成功使用validcredentials(){
当(mMockViewContract.getUsername())。然后返回(“steve”);
当(mMockViewContract.getPassword())。然后返回(“1234”);
mloginPresentContract.validateCredentials();
验证(mMockViewContract,times(1)).getUsername();
验证(mMockViewContract,次(1)).getPassword();
验证(mMockOnLoginCompletedListener,次(1)).onSuccess();
验证(mMockOnLoginCompletedListener,never()).onPasswordError();
验证(mMockOnLoginCompletedListener,never()).onUsernameError();
}
}
有没有办法测试这个实现


非常感谢您的建议,

我可能没有领会您的意思,但是您是否尝试过使用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类加载xstream:1.6.5”
然后这样使用它:

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

}

测试类
LoginPresentImpTest
是关于
LoginPresentImp
类的测试,它应该只使用它的实际实现和它的合作者的模拟。类
LoginModelContract.OnLoginCompletedListener
LoginModelImp
的合作者,因此在设计良好且纯粹的
loginpresentremp
单元测试中,像您一样,从未调用它是完全正常的。 我建议的解决方案是单独测试LoginModelImp:

@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");
}
或者,您必须在
loginpresentremptest
中使用
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();
    }
}
public类loginpresentremotest{
private LoginFragmentViewContract mMockViewContract;
私人LoginModelContract mModelContract;
私有LoginModelContract.OnLoginCompletedListener mMockOnLoginCompletedListener;
私人LoginResenterContract MLLoginResenterContract;
@以前
public void setUp()引发异常{
mMockViewContract=Mockito.mock(LoginFragmentViewContract.class);
mModelContract=new LoginModelImp();
loginpresentremp spyppresenterimp=Mockito.spy(新的loginpresentremp(mModelContract));
mLoginPresenterContract=SpypPresenterImp;
mMockOnLoginCompletedListener=spyppresenterimp;
mloginpresentract.attachView(mMockViewContract);
}
@试验
public void应成功使用validcredentials(){
当(mMockViewContract.getUsername())。然后返回(“steve”);
当(mMockViewContract.getPassword())。然后返回(“1234”);
mloginPresentContract.validateCredentials();
验证(mMockViewContract,times(1)).getUsername();
验证(mMockViewContract,次(1)).getPassword();
验证(mMockOnLoginCompletedListener,次(1)).onSuccess();
验证(mMockOnLoginCompletedListener,never()).onPasswordError();
验证(mMockOnLoginCompletedListener,never()).onUsernameError();
}
}

我认为,因为您在模拟
LoginModelContract
OnLoginCompletedListener
您不能断言
onUsernameError
onPasswordError
onSuccess
实际上是被调用的,因为通过模拟
LoginModelContract
这个“真正的”登录方法(应该调用这些方法的)将不会执行,但只调用模拟方法。 您可以通过以下方式触发这些方法:

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

这归结为用户故事和用例之间的差异。在这种情况下,您有一个用户故事(例如,“作为用户,我想登录,所以我提供用户名和密码”),但实际上至少有3个用例:正确的用户名/正确的密码、正确的用户名/错误的密码、错误的用户名/正确的密码等。作为一般最佳实践,您希望测试与用例的比例为1:1,因此我建议如下:

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();
    }
}
换言之,对于测试1,您正在尝试积极验证用户名和密码完成后是否调用了成功。对于测试2,您正在验证调用onUsernameError的条件,对于测试3,您正在验证调用onPasswordError的条件。这三个条件都是要测试的有效内容,您希望验证调用它们是正确的,但是需要将它们视为不同的用例

为了完整性,我将验证错误的用户/错误的密码会发生什么,并且还将验证错误的密码会发生什么
 @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();
}