使用 Android 注释、Mockito 和 MVP 模式时的单元测试
Unittesting when using Android Annotations, Mockito and MVP pattern
直到昨天,我已经使用 MVP 模式和 Android Annotations library.
成功地组合了一个非常易读的 Android 项目
但是昨天,当我开始为我的 LoginPresenter 编写单元测试时,出现了一个问题。
首先是我的 LoginPresenter 中的一些代码。
...
@EBean
public class LoginPresenterImpl implements LoginPresenter, LoginInteractor.OnLoginFinishedListener {
@RootContext
protected LoginActivity loginView;
@Bean(LoginInteractorImpl.class)
LoginInteractor loginInteractor;
@Override public void validateCredentials(String username, String password) {
if (loginView != null) {
loginView.showProgress();
}
if (TextUtils.isEmpty(username)) {
// Check that username isn't empty
onUsernameError();
}
if (TextUtils.isEmpty(password)){
// Check that password isn't empty
onPasswordError();
// No reason to continue to do login
} else {
}
}
@UiThread(propagation = UiThread.Propagation.REUSE)
@Override public void onUsernameError() {
if (loginView != null) {
loginView.setUsernameError();
loginView.hideProgress();
}
}
...
我的测试:
@RunWith(MockitoJUnitRunner.class)
public class LoginPresenterImplTest {
private LoginPresenter loginPresenter;
@Mock
private LoginPresenter.View loginView;
@Before
public void setUp() {
// mock or create a Context object
Context context = new MockContext();
loginPresenter = LoginPresenterImpl_.getInstance_(context);
MockitoAnnotations.initMocks(this);
}
@After
public void tearDown() throws Exception {
loginPresenter = null;
}
@Test
public void whenUserNameIsEmptyShowUsernameError() throws Exception {
loginPresenter.validateCredentials("", "testtest");
// verify(loginPresenter).onUsernameError();
verify(loginView).setUsernameError();
}
}
问题是我没有使用使用 MVP 模式的标准方法,而是尝试使用 Android 注释来使代码更具可读性。所以我没有使用 attachView() 或 detachView() 方法将我的演示者附加到我的 LoginActivity(视图)。这意味着我不能嘲笑我的 "view"。有人知道这个问题的解决方法吗? 运行 测试时,我不断收到以下消息:
Wanted but not invoked:
loginView.setUsernameError();
-> at com.conhea.smartgfr.login.LoginPresenterImplTest.whenUserNameIsEmptyShowUsernameError(LoginPresenterImplTest.java:48)
Actually, there were zero interactions with this mock.
解决方案(我不再使用@RootContext):
主持人:
@EBean
public class LoginPresenterImpl extends AbstractPresenter<LoginPresenter.View>
implements LoginPresenter, LoginInteractor.OnLoginFinishedListener {
private static final String TAG = LoginPresenterImpl.class.getSimpleName();
@StringRes(R.string.activity_login_authenticating)
String mAuthenticatingString;
@StringRes(R.string.activity_login_aborting)
String mAbortingString;
@StringRes(R.string.activity_login_invalid_login)
String mInvalidCredentialsString;
@StringRes(R.string.activity_login_aborted)
String mAbortedString;
@Inject
LoginInteractor mLoginInteractor;
@Override
protected void initializeDagger() {
Log.d(TAG, "Initializing Dagger injection");
Log.d(TAG, "Application is :" + getApp().getClass().getSimpleName());
Log.d(TAG, "Component is: " + getApp().getComponent().getClass().getSimpleName());
Log.d(TAG, "UserRepo is: " + getApp().getComponent().userRepository().toString());
mLoginInteractor = getApp().getComponent().loginInteractor();
Log.d(TAG, "LoginInteractor is: " + mLoginInteractor.getClass().getSimpleName());
}
@Override
public void validateCredentials(String username, String password) {
boolean error = false;
if (!isConnected()) {
noNetworkFailure();
error = true;
}
if (TextUtils.isEmpty(username.trim())) {
// Check that username isn't empty
onUsernameError();
error = true;
}
if (TextUtils.isEmpty(password.trim())) {
// Check that password isn't empty
onPasswordError();
error = true;
}
if (!error) {
getView().showProgress(mAuthenticatingString);
mLoginInteractor.login(username, password, this);
}
}
...
我的测试(其中一些):
@RunWith(AppRobolectricRunner.class)
@Config(constants = BuildConfig.class)
public class LoginPresenterImplTest {
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
private LoginPresenterImpl_ mLoginPresenter;
@Mock
private LoginPresenter.View mLoginViewMock;
@Mock
private LoginInteractor mLoginInteractorMock;
@Captor
private ArgumentCaptor<LoginInteractor.OnLoginFinishedListener> mCaptor;
@Before
public void setUp() {
mLoginPresenter = LoginPresenterImpl_.getInstance_(RuntimeEnvironment.application);
mLoginPresenter.attachView(mLoginViewMock);
mLoginPresenter.mLoginInteractor = mLoginInteractorMock;
}
@After
public void tearDown() throws Exception {
mLoginPresenter.detachView();
mLoginPresenter = null;
}
@Test
public void whenUsernameAndPasswordIsValid_shouldLogin() throws Exception {
String authToken = "Success";
mLoginPresenter.validateCredentials("test", "testtest");
verify(mLoginInteractorMock, times(1)).login(
anyString(),
anyString(),
mCaptor.capture());
mCaptor.getValue().onSuccess(authToken);
verify(mLoginViewMock, times(1)).loginSuccess(authToken);
verify(mLoginViewMock, times(1)).hideProgress();
}
@Test
public void whenUsernameIsEmpty_shouldShowUsernameError() throws Exception {
mLoginPresenter.validateCredentials("", "testtest");
verify(mLoginViewMock, times(1)).setUsernameError();
verify(mLoginViewMock, never()).setPasswordError();
verify(mLoginViewMock, never()).hideProgress();
}
...
作为一种解决方法,您可以采用以下方法:
public class LoginPresenterImpl ... {
...
@VisibleForTesting
public void setLoginPresenter(LoginPresenter.View loginView) {
this.loginView = loginView;
}
}
测试中class:
@Before
public void setUp() {
...
MockitoAnnotations.initMocks(this);
loginPresenter.setLoginPresenter(loginView);
}
但是,根据经验,当您看到 @VisibleForTesting
注释时,这意味着您的架构有问题。最好重构你的项目。
请注意想要在其项目中使用 Android 注释的开发人员。编写单元测试时请注意您的代码不会访问 Android API。 Android 注释的底层实现严重依赖于 Android API。因此,自动生成的代码可能依赖于此,并且难以编写单元测试。
永远记住,Android 注释将您的 class 替换为最终的 class,在其 class 名称的末尾添加了一个 _。在此生成的 class 中,根据原始 class 的注释方式自动生成大量样板代码。在我的例子中,问题是我正在做一个 Android-project 并且想要我的很多方法从我的演示者到 运行 在 UI-thread 上。这是使用 Android 注释和 @UIThread 注释实现的。但这意味着我的方法实际上是用另一个调用 super-class:
的方法包装的
@Override
public void onUsernameError() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
LoginPresenterImpl_.super.onUsernameError();
return;
}
UiThreadExecutor.runTask("", new Runnable() {
@Override
public void run() {
LoginPresenterImpl_.super.onUsernameError();
}
}
, 0L);
}
我的测试用例过不了线:
...
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
...
这当然是因为我们无法在简单的单元测试中访问 Android API。所以问题就出在这里。
结论:在为使用 Android 注释的项目编写单元测试时必须非常小心,自动生成的代码不依赖于 Android 相关 API。
使用androids TextUtil-class也是同样的问题。
直到昨天,我已经使用 MVP 模式和 Android Annotations library.
成功地组合了一个非常易读的 Android 项目但是昨天,当我开始为我的 LoginPresenter 编写单元测试时,出现了一个问题。
首先是我的 LoginPresenter 中的一些代码。
...
@EBean
public class LoginPresenterImpl implements LoginPresenter, LoginInteractor.OnLoginFinishedListener {
@RootContext
protected LoginActivity loginView;
@Bean(LoginInteractorImpl.class)
LoginInteractor loginInteractor;
@Override public void validateCredentials(String username, String password) {
if (loginView != null) {
loginView.showProgress();
}
if (TextUtils.isEmpty(username)) {
// Check that username isn't empty
onUsernameError();
}
if (TextUtils.isEmpty(password)){
// Check that password isn't empty
onPasswordError();
// No reason to continue to do login
} else {
}
}
@UiThread(propagation = UiThread.Propagation.REUSE)
@Override public void onUsernameError() {
if (loginView != null) {
loginView.setUsernameError();
loginView.hideProgress();
}
}
...
我的测试:
@RunWith(MockitoJUnitRunner.class)
public class LoginPresenterImplTest {
private LoginPresenter loginPresenter;
@Mock
private LoginPresenter.View loginView;
@Before
public void setUp() {
// mock or create a Context object
Context context = new MockContext();
loginPresenter = LoginPresenterImpl_.getInstance_(context);
MockitoAnnotations.initMocks(this);
}
@After
public void tearDown() throws Exception {
loginPresenter = null;
}
@Test
public void whenUserNameIsEmptyShowUsernameError() throws Exception {
loginPresenter.validateCredentials("", "testtest");
// verify(loginPresenter).onUsernameError();
verify(loginView).setUsernameError();
}
}
问题是我没有使用使用 MVP 模式的标准方法,而是尝试使用 Android 注释来使代码更具可读性。所以我没有使用 attachView() 或 detachView() 方法将我的演示者附加到我的 LoginActivity(视图)。这意味着我不能嘲笑我的 "view"。有人知道这个问题的解决方法吗? 运行 测试时,我不断收到以下消息:
Wanted but not invoked:
loginView.setUsernameError();
-> at com.conhea.smartgfr.login.LoginPresenterImplTest.whenUserNameIsEmptyShowUsernameError(LoginPresenterImplTest.java:48)
Actually, there were zero interactions with this mock.
解决方案(我不再使用@RootContext):
主持人:
@EBean
public class LoginPresenterImpl extends AbstractPresenter<LoginPresenter.View>
implements LoginPresenter, LoginInteractor.OnLoginFinishedListener {
private static final String TAG = LoginPresenterImpl.class.getSimpleName();
@StringRes(R.string.activity_login_authenticating)
String mAuthenticatingString;
@StringRes(R.string.activity_login_aborting)
String mAbortingString;
@StringRes(R.string.activity_login_invalid_login)
String mInvalidCredentialsString;
@StringRes(R.string.activity_login_aborted)
String mAbortedString;
@Inject
LoginInteractor mLoginInteractor;
@Override
protected void initializeDagger() {
Log.d(TAG, "Initializing Dagger injection");
Log.d(TAG, "Application is :" + getApp().getClass().getSimpleName());
Log.d(TAG, "Component is: " + getApp().getComponent().getClass().getSimpleName());
Log.d(TAG, "UserRepo is: " + getApp().getComponent().userRepository().toString());
mLoginInteractor = getApp().getComponent().loginInteractor();
Log.d(TAG, "LoginInteractor is: " + mLoginInteractor.getClass().getSimpleName());
}
@Override
public void validateCredentials(String username, String password) {
boolean error = false;
if (!isConnected()) {
noNetworkFailure();
error = true;
}
if (TextUtils.isEmpty(username.trim())) {
// Check that username isn't empty
onUsernameError();
error = true;
}
if (TextUtils.isEmpty(password.trim())) {
// Check that password isn't empty
onPasswordError();
error = true;
}
if (!error) {
getView().showProgress(mAuthenticatingString);
mLoginInteractor.login(username, password, this);
}
}
...
我的测试(其中一些):
@RunWith(AppRobolectricRunner.class)
@Config(constants = BuildConfig.class)
public class LoginPresenterImplTest {
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
private LoginPresenterImpl_ mLoginPresenter;
@Mock
private LoginPresenter.View mLoginViewMock;
@Mock
private LoginInteractor mLoginInteractorMock;
@Captor
private ArgumentCaptor<LoginInteractor.OnLoginFinishedListener> mCaptor;
@Before
public void setUp() {
mLoginPresenter = LoginPresenterImpl_.getInstance_(RuntimeEnvironment.application);
mLoginPresenter.attachView(mLoginViewMock);
mLoginPresenter.mLoginInteractor = mLoginInteractorMock;
}
@After
public void tearDown() throws Exception {
mLoginPresenter.detachView();
mLoginPresenter = null;
}
@Test
public void whenUsernameAndPasswordIsValid_shouldLogin() throws Exception {
String authToken = "Success";
mLoginPresenter.validateCredentials("test", "testtest");
verify(mLoginInteractorMock, times(1)).login(
anyString(),
anyString(),
mCaptor.capture());
mCaptor.getValue().onSuccess(authToken);
verify(mLoginViewMock, times(1)).loginSuccess(authToken);
verify(mLoginViewMock, times(1)).hideProgress();
}
@Test
public void whenUsernameIsEmpty_shouldShowUsernameError() throws Exception {
mLoginPresenter.validateCredentials("", "testtest");
verify(mLoginViewMock, times(1)).setUsernameError();
verify(mLoginViewMock, never()).setPasswordError();
verify(mLoginViewMock, never()).hideProgress();
}
...
作为一种解决方法,您可以采用以下方法:
public class LoginPresenterImpl ... {
...
@VisibleForTesting
public void setLoginPresenter(LoginPresenter.View loginView) {
this.loginView = loginView;
}
}
测试中class:
@Before
public void setUp() {
...
MockitoAnnotations.initMocks(this);
loginPresenter.setLoginPresenter(loginView);
}
但是,根据经验,当您看到 @VisibleForTesting
注释时,这意味着您的架构有问题。最好重构你的项目。
请注意想要在其项目中使用 Android 注释的开发人员。编写单元测试时请注意您的代码不会访问 Android API。 Android 注释的底层实现严重依赖于 Android API。因此,自动生成的代码可能依赖于此,并且难以编写单元测试。
永远记住,Android 注释将您的 class 替换为最终的 class,在其 class 名称的末尾添加了一个 _。在此生成的 class 中,根据原始 class 的注释方式自动生成大量样板代码。在我的例子中,问题是我正在做一个 Android-project 并且想要我的很多方法从我的演示者到 运行 在 UI-thread 上。这是使用 Android 注释和 @UIThread 注释实现的。但这意味着我的方法实际上是用另一个调用 super-class:
的方法包装的@Override
public void onUsernameError() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
LoginPresenterImpl_.super.onUsernameError();
return;
}
UiThreadExecutor.runTask("", new Runnable() {
@Override
public void run() {
LoginPresenterImpl_.super.onUsernameError();
}
}
, 0L);
}
我的测试用例过不了线:
...
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
...
这当然是因为我们无法在简单的单元测试中访问 Android API。所以问题就出在这里。
结论:在为使用 Android 注释的项目编写单元测试时必须非常小心,自动生成的代码不依赖于 Android 相关 API。
使用androids TextUtil-class也是同样的问题。