1

直到昨天,我才使用 MVP 模式和Android Annotations 库成功地组合了一个可读性很强的 Android 项目。

但是昨天当我开始为我的 LoginPresenter 编写 unittest 时,问题就出现了。

首先是我的 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 Annotations 以使代码更具可读性。所以我没有使用 attachView() 或 detachView() 方法将我的演示者附加到我的 LoginActivity(视图)。这意味着我不能嘲笑我的“观点”。有人知道这个问题的解决方法吗?运行测试时,我不断收到以下消息:

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

2 回答 2

0

作为一种解决方法,您可以这样做:

public class LoginPresenterImpl ... {

    ...

    @VisibleForTesting
    public void setLoginPresenter(LoginPresenter.View loginView) {
        this.loginView = loginView;
    }

}

在测试类中:

@Before
public void setUp() {
    ...

    MockitoAnnotations.initMocks(this);
    loginPresenter.setLoginPresenter(loginView);
}

但是,根据经验,当您看到@VisibleForTesting注释时,这意味着您的架构有问题。最好重构你的项目。

于 2017-04-04T15:04:17.110 回答
0

向希望在其项目中使用 Android 注释的开发人员提供帮助。编写单元测试时请注意您的代码无法访问 Android API。Android Annotations 的底层实现很大程度上依赖于 Android API。因此,自动生成的代码可能依赖于此,并且难以编写单元测试。

永远记住,Android Annotations 将您的类替换为在其类名末尾添加 _ 的最终类。在这个生成的类中,许多样板代码是根据原始类的注释方式自动生成的。就我而言,问题是我正在开发一个 Android 项目,并且希望我的演示者提供的许多方法在 UI 线程上运行。这是通过使用 @UIThread 注释的 Android 注释来实现的。但这意味着我的方法实际上是用另一个调用超类的方法包装的:

@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 Annotations 为项目编写单元测试时必须非常小心,自动生成的代码不依赖于 Android 相关的 API。

使用androids TextUtil-class时也是同样的问题。

于 2017-04-06T12:20:56.970 回答