前言
如果你在网上搜 哪种项目架构更好
的时候, 会看到成百上千的博客对各种架构解释优缺点。 但是不幸的是大多数文章都没有提到非常重要的一点: 单元测试
在我们选择某一种项目架构的时候,起决定性因素的无非是个人喜好或者项目需求。我并不认为 MVP
架构比 MVVM
架构更好,或者说 MVP
架构就是一种完美的客户解决方案。让我决定使用 MVP
架构的唯一理由就是它的 简洁性
MVP
MVP
代表 Model-View-PresenterModel
通常理解为data source(数据来源),不管是来自网络或者数据库,甚至是手写的一个List对象也可以被认为是一个数据来源View
一般可以使用Activity、Fragment或者是一个自定义View来充当。View层的主要功能就是展示界面,拦截用户交互事件Presenter
在其中扮演着 POJO (plain old java object) 的角色。它负责View
层和Model
层的通信。
注意:
在我们去实现某一个 Presenter
的时候,一定要注意将 Model
层可能出现的 Error
信息交给 Presenter
去做统一处理。 并且尽量将业务逻辑从UI层抽离出,放到 Presenter
中去实现
MVP架构测试原则
- 首要原则就是要使用JUnit而不是Espresso或者其他的三方自动化测试框架
- 其次是对每一层都单独分开测,这一点与集成化测试时截然相反的。因此需要对一些 依赖性注入框架 有一定的了解。我推荐的是 目前火热的
Dagger
框架 - 由于我们使用的是JUnit,因此我们需要一个单独测试 UI 功能的框架。 对于此我比较推荐的是目前比较成熟的 Robolectric 框架。
- 对
Mockito
框架的使用也是必须的,因为它基本是目前最流行的Mock测试框架了
测试 Model层
Model不能持有 Presenter 和 View 层的引用
Model在Presenter层应该尽量以一个简单的接口的形式存在,尤其是当我们使用三方框架的时候
Model层的测试永远都不应该对项目架构有所依赖,它应该是独立的
案例:
创建Model接口向用户提供相关数据(我们不知道这些数据是来自网络还是数据库), 因为使用RxJava 所以返回类型是Observable
public interface ProfileInteractor {
Observable<UserProfile> getProfile();
}
而测试这个接口方法的话可以直接使用RxJava
提供给我们的 TestSubscriber
类, 具体如下所示:
public class ProfileInteractorTest {
private static final String USER = "USERNAME";
ProfileInteractor interactor;
@Before
public void setUp() {
interactor = new ProfileInteractorImpl(...);
}
@Test
public void testGetUserProfile() throws Exception {
TestSubscriber<UserProfile> subscriber = TestSubscriber.create();
interactor.getProfile().subscribe(subscriber);
subscriber.assertNoErrors();
subscriber.assertCompleted();
assertThat(subscriber.getOnNextEvents().get(0).getName()).isEqualTo(USER);
}
}
测试 View 层
对 View 层的测试相对简单一些,难点在于对 Robolectric
的配置。首先还是来看下 View 接口
public interface ProfileView {
void display(UserProfile userProfile);
}
我们选择的案例是使用一个Android 自定义View来充当 View层
public class ProfileFrameLayout extends FrameLayout implements ProfileView {
private ProfilePresenter presenter;
@BindView(R.id.text_username)
TextView textUsername;
@Inject
public void setPresenter(ProfilePresenter presenter) {
this.presenter = presenter;
presenter.attachView(this);
}
public CollectionFrameLayout(Context context) {
super(context);
init();
}
public CollectionFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
View view = inflate(getContext(), R.layout.view_profile, this);
ButterKnife.bind(view);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
presenter.attachView(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
presenter.detachView();
}
@Override
public void display(UserProfile userProfile) {
textUsername.setText(userProfile.getName());
}
}
那么问题来了: 对于这个 View
我们应该测试哪些部分或者说是哪些代码应该写测试代码呢?
答案是:EVERYTHING !!所有的都必须测试到位
1 测试 View 是否被成功创建
2 测试默认值是否正确
3 测试用户交互是否正确的传递给了Presenter
4 测试View只是做它应该做的事情(展示界面)
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class ProfileFrameLayoutTest {
private ProfileFrameLayout profileView;
@Mock
ProfilePresenter presenter;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
profileView = new ProfileFrameLayout(RuntimeEnvironment.application);
profileView.setPresenter(presenter);
}
@Test
public void testEmpty() throws Exception {
verify(presenter).attachView(profileView);
asserThat(profileView.textUsername.getText().toString()).isEmpty();
}
@Test
public void testLeaveView() throws Exception {
profileView.onDetachedFromWindow();
verify(presenter).detachView();
}
@Test
public void testReturnToView() throws Exception {
reset(presenter);
profileView.onAttachedToWindow();
verify(presenter).attachView(profileView);
}
@Test
public void testDisplay() throws Exception {
UserProfile user = new UserProfile(USER);
profileView.display(user);
asserThat(profileView.textUsername.getText().toString()).isEqualTo(USER);
}
}
解释:
- 最上面的注解是对Robolectrie的配置
- 声明一个 ProfileFrameLayout 的全局引用,主要就是对它进行测试
- 使用Mockito创建一个mock的Presenter,因为我们只是想通过它来验证 View 层是否会成功调用到 Presenter层的相关代码
- 因为android代码中创建View的时候都必须传入一个Context上下文,因此我们使用Robolectric提供给我们的RuntimeEnviroment来作为上下文,并传给ProfileFrameLayout对象,并将Presenter对象传递给它
测试 Presenter 层
测试Presenter
和测试Model
的方式差不多 只不过这次我们是对View层进行mock。 Presenter将接收一个View对象和一个Model层的接口对象,分别用来展示界面和获取数据
public void ProfilePresenter {
private final ProfileInteractor interactor;
private ProfileView view;
public ProfilePresenter(ProfileInteractor interactor) {
this.interactor = interactor.
}
public void attachView(ProfileView view) {
this.view = view;
fetchAndDisplay();
}
public void dettachView() {
// Not covered by this example:
// You should handle the subscription
}
public void fetchAndDisplay() {
// Not covered by this example:
// You should handle the subscription
// You should also check if view is not null
// You should also handle the onError
interactor.getUserProfile().subscribe(userProfile -> view.display(userProfile));
}
}
对于 Presenter
层的测试,我们的主要目标是保证成功从 Model
层拿到数据并成功传递给 View
层展示, 对于数据是否正确我们并不是很在乎。
但是如果你在Presenter层对数据进行了一个转化,那么你就需要对数据的正确性也进行一些相关验证
代码如下:
public void ProfilePresenterTest {
@Mock
ProfileInteractor interactor;
@Mock
ProfileView view;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(interactor.getUserProfile()).thenReturn(Observable.just(new UserProfile()));
presenter = new ProfilePresenter(interactor);
presenter.attachView(view);
}
@Test
public void testDisplayCalled() {
verify(interactor).getUserProfile();
verify(view).display(any());
}
}
总结
- 创建一个Mockable的
Model
, 如果不行就对它进行再抽象再封装 - 使用依赖注入框架(Dagger)自动向 View 层注入Presenter对象,不用在View中手动创建Presenter对象
- 不要只关注测试输出数据,也要关注对象之间的交互
- 如果Presenter对View的声明周期也有依赖,那我们就必须对其进行测试
- Test visual changes from your View, not only the text, but also visibility or background color if you change it.对View层的测试要细致到极点,不仅仅是对Text文本的测试,还要对背景颜色,可见性等进行测试
- 测试Presenter对Model层提供不同数据时的不同响应
参考链接:https://medium.com/@Miqubel/testing-android-mvp-aa0de6e165e4