使用 mockito 和事件监听器进行单元测试 MVP
Unit Testing MVP using mockito with event listeners
Android Studio 2.1.2
我想测试一下 LoginModelImp 中的回调 onUsernameError、onPasswordError 和 onSuccess 是否真的被调用了。我不确定如何测试事件侦听器。但是,测试失败,因为这些函数从未被调用过。我正在用 mockito 嘲笑他们并试图验证他们。
到目前为止,这是我的代码。
演示者界面
public interface LoginPresenterContract<LoginFragmentViewContract> {
void validateCredentials();
void attachView(LoginFragmentViewContract view);
void detachView();
}
Presenter 实现
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();
}
}
}
使用 Mockito 进行 JUnit4 测试
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();
}
}
有什么方法可以测试这个实现吗?
非常感谢您的任何建议,
我可能没有理解你的意思,但你试过使用 PowerMock 吗?
您需要以下依赖项:
- 测试编译"org.powermock:powermock-module-junit4:1.6.5"
- 测试编译 "org.powermock:powermock-module-junit4-rule:1.6.5"
- 测试编译"org.powermock:powermock-api-mockito:1.6.5"
- 测试编译"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");
}
}
测试 class LoginPresenterImpTest
是关于 LoginPresenterImp
class 的测试,它应该只使用它的实际实现和它的合作者的模拟。 class 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();
}
}
或者,您必须在 LoginPresenterImpTest
中使用 LoginModelImp
的实际实现并监视您的侦听器(即演示者本身)或配置模拟以使它们调用侦听器.这是一个例子,但我不会使用这个:
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();
}
}
我认为因为你在嘲笑 LoginModelContract
和 OnLoginCompletedListener
你不能断言 onUsernameError
、onPasswordError
和 onSuccess
实际上被调用了,因为通过模拟 LoginModelContract
,"real" 登录方法(应该调用这些方法)将不会被执行,而只会调用模拟的方法。
您可以使用以下内容触发这些方法:
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();
}
}
这归结为用户故事和用例之间的区别。在这种情况下,您有 1 个用户故事(例如 "As a User, I want to Login, so I provide my Username and Password"),但实际上至少有 3 个用例:正确的 Username/Right 密码,正确的 Username/Wrong 密码,错误的 Username/Right密码等。作为一般的最佳实践,您希望测试与用例对应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,您正在尝试积极验证,当用户名和密码完成时,调用成功。对于测试 2,您正在验证调用 onUsernameError 的条件,对于测试 3,验证调用 onPasswordError 的条件。这三个都是要测试的有效事物,您想要验证它们是否被调用是正确的,但您需要将它们视为不同的用例。
为了完整起见,我将验证 Wrong_User/Wrong_Password 上发生的情况,并验证如果出现 N 次 Wrong_Password 情况会发生什么(您需要冻结帐户吗?)。
希望这对您有所帮助。祝你好运。
Android Studio 2.1.2
我想测试一下 LoginModelImp 中的回调 onUsernameError、onPasswordError 和 onSuccess 是否真的被调用了。我不确定如何测试事件侦听器。但是,测试失败,因为这些函数从未被调用过。我正在用 mockito 嘲笑他们并试图验证他们。
到目前为止,这是我的代码。
演示者界面
public interface LoginPresenterContract<LoginFragmentViewContract> {
void validateCredentials();
void attachView(LoginFragmentViewContract view);
void detachView();
}
Presenter 实现
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();
}
}
}
使用 Mockito 进行 JUnit4 测试
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();
}
}
有什么方法可以测试这个实现吗?
非常感谢您的任何建议,
我可能没有理解你的意思,但你试过使用 PowerMock 吗?
您需要以下依赖项:
- 测试编译"org.powermock:powermock-module-junit4:1.6.5"
- 测试编译 "org.powermock:powermock-module-junit4-rule:1.6.5"
- 测试编译"org.powermock:powermock-api-mockito:1.6.5"
- 测试编译"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");
}
}
测试 class LoginPresenterImpTest
是关于 LoginPresenterImp
class 的测试,它应该只使用它的实际实现和它的合作者的模拟。 class 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();
}
}
或者,您必须在 LoginPresenterImpTest
中使用 LoginModelImp
的实际实现并监视您的侦听器(即演示者本身)或配置模拟以使它们调用侦听器.这是一个例子,但我不会使用这个:
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();
}
}
我认为因为你在嘲笑 LoginModelContract
和 OnLoginCompletedListener
你不能断言 onUsernameError
、onPasswordError
和 onSuccess
实际上被调用了,因为通过模拟 LoginModelContract
,"real" 登录方法(应该调用这些方法)将不会被执行,而只会调用模拟的方法。
您可以使用以下内容触发这些方法:
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();
}
}
这归结为用户故事和用例之间的区别。在这种情况下,您有 1 个用户故事(例如 "As a User, I want to Login, so I provide my Username and Password"),但实际上至少有 3 个用例:正确的 Username/Right 密码,正确的 Username/Wrong 密码,错误的 Username/Right密码等。作为一般的最佳实践,您希望测试与用例对应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,您正在尝试积极验证,当用户名和密码完成时,调用成功。对于测试 2,您正在验证调用 onUsernameError 的条件,对于测试 3,验证调用 onPasswordError 的条件。这三个都是要测试的有效事物,您想要验证它们是否被调用是正确的,但您需要将它们视为不同的用例。
为了完整起见,我将验证 Wrong_User/Wrong_Password 上发生的情况,并验证如果出现 N 次 Wrong_Password 情况会发生什么(您需要冻结帐户吗?)。
希望这对您有所帮助。祝你好运。