Home 100% 覆盖测试Android MVP:
Post
Cancel

100% 覆盖测试Android MVP:

Testing Android MVP

网上有成千上万的文章分析 Android 上各种架构模式的优缺点,然后让人失望的是它们都忽略了一点:测试

选择哪种模式通常取决于个人喜好和项目诉求。我不认为 MVVM 优于 MVP 或者说它是一个能完全满足用户需求的解决方案,当然,由于 MVVM 的简洁,在启动新项目时我会优先选择它。

Great Fragments battle between the Hungarian Notation and the MVPs

MVP 的四个要点( bullet point)

  • MVP 指 Model-View-Presenter
  • Model 是数据源。不管它是存于 DB,网络 还是本地内存
  • View 是 Activity / Fragment 或自定义 View 。它应该只关心显示及处理用户交互
  • Presenter 是个普通 Java 对象,它负责处理 View 和 Model 的通信,处理来自 Model 的数据转换,错误,并尽可能地减少 View 层的交互逻辑

MVP 测试原则

  • 首要原则 :仅使用 JUnit 而非 Espresso 或其他自动化测试框架
  • 其次:每一部分都单独测试,不使用集成测试。所以这个需要依赖注入框架相关技能。
  • 因为使用 JUnit ,我们需要 Mock 所有 UI 组件。因此我们要用到 Robolectric
  • 我们还需要熟练使用 Mockito ,因为我们要测试交互及 mock 类。

测试 Model

不管使不使用 MVP ,测试 Model 都应该是独立进行的事。在某些场景下,我们的 Model 是第三方库,因此我们无法进行测试。但我们应该能提供易于 mock 的接口。

  • Model 不应引用任何Presenter 或 View 对象。
  • Model 应提供易于 mock 的接口
  • 不管用什么设计模式,Model 都应可独立测试

例:

以下Model提供用户资料(不管它是从网络还是DB):

1
2
3
interface ProfileInterator{
  fun getProfile():Observable<UserProfile> 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ProfileInteractorTest{
  val USER = "USERNAME"
  latinit var interactor:ProfileInteractor

  @Before
  fun setUp(){
    interactor = ProfileInteractorImpl(...)
  }

  @Test
  fun testGetUserProfile(){
    val subscriber = TestSubscriber<UserProfile>.create()
    interactor.getProfile().subscribe(subscriber)
    subscriber.apply{
      assertNoErrors()
      assertCompleted()
      assertThat(
        getOnNextEvents().get(0).getName()).equalTo(USER)
      )
    }
  }
}

译注:这个例子用了 RxJava 的 TestSubscriber,不使用框架怎么办?

测试View

测试View 会比我们所想的要简单,它最复杂的部分是 Robolectric 的使用,但在此之后一切都会非常简单。

考虑以下 View 接口:

1
2
3
interface ProfileView{
  fun display(userProfile:UserProfile)
}

具体的View 是一个自定义的 FrameLayout :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ProfileFrameLayout @JvmOverloads constructor(
  context: Context, 
  attrs: AttributeSet? = null) : Framelayout(
    context, attrs
    ),ProfileView{

  private lateinit var view:View

  lateinit var textUserName:TextView

  var presenter:ProfilePresenter? = null
    set(value){
        field = value
      value.attachView(this)
    }

  init {
    view = inflate(context,R.layout.view_profile,this)
    textUserName = findViewById(R.id.text_user_name)
    ButterKnife.bind(view)
  }

  override fun onAttachedToWindow(){
    super.onAttachedToWindow()
    presenter.attachView(this)
  }
  override fun onDetachedFromWindow(){
    super.onDetachedFromWindow()
    presenter.detachView()
  }
  override fun display(userProfile:UserProfile){
    textUserName.setText(userProfile.getName())
  }

}

译注:原文使用了 @Inject ,翻译成 kotlin 未加上

以上 View 层要测试什么?所有内容

  • View 被正确创建
  • 有正确的默认值
  • 用户操作会触发 Presenter 的操作
  • View 能正确实现其功能(显示用户信息)

具体测试代码(讲解在代码后方)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@RunWith(RobolectricGradleTestRunner::class.java)
@Config(constants = BuildConfig::class.java)
class ProfileFrameLayoutTest {
  private lateinit var profileView:ProfileFrameLayout

  @Mock
  lateinit var presenter:ProfilePresenter

  @Before
  fun setUp() {
    MockitoAnnotations.initMocks(this)
    profileView = ProfileFrameLayout(RuntimeEnvironment.application)
    profileView.setPresenter(presenter)
  }

  @Test
  fun testEmpty() {
    verify(presenter).attachView(profileView)
    assertThat(profileView.textUserName.text.toString().isEmpty())
  }

  @Test testLeaveView() {
    profileView.onDetachedFromWindow()
    verify(presenter).detachView()
  }

  @Test
  fun testReturnToView() {
    reset(presenter)
    profileView.onAttachedToWindow()
    verify(presenter).attachView(profileView)
  }

  @Test
  fun testDisplay() {
    val user = UserProfile(USER)
    profileView.display(user)
    assertThat(
      profileView.textUserName.text.toString()
    ).isEqualTo(USER)
  }

}

以上代码实现:

  • 最顶上的注解是 Robolectric 的配置。告诉 JUnit 正在使用默认配置进行 Robolectric 测试
  • 持有一个 view 引用进行测试
  • Mock 一个 presenter ,我们只需要知道 presenter 的接口在正确的时间被正确地调用
  • 持有一个 ProfileFrameLayout 对象,创建时使用 RuntimeEnviroment 的 application
  • 给 View 设置了被 mock 的 presenter
  • 几个 @Test 就不说了

如此,我们的测试覆盖了 View 的所有代码。

小结

  • 提供 Mock 的 Presenter 来验证对应在的事件或交互发生时,presenter 有没调用对应接口
  • 提供View 内部的对象(译注:这需要打破封装,有没更好做法?)以检查其状态和值
  • 通过 Robolectric 创建View ,以移除Android 的依赖

测试 Presenter

我们用和测试 Model 相同的方法来测试 Presenter ,只需简单使用 JUnit 。不过这次我们需要提供一个 mock 的 View 来验证 Presenter 是否正确地与 View 进行通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ProfilePresenter(interactor) {
  val interactor:ProfileInteractor = interactor

  private lateinit var view:ProfileView? = null

  fun attachView(view:ProfileVew) {
    this.view = view
    fetchAndDisplay()
  }

  fun detachView() {
    // 本例中不覆盖
    // 应该处理订阅
  }

  fun 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{ profile ->
      view.display(profile)
    }
  }


}

上例程为文章简洁未处理很多 RxJava 相关的内容。

这里我们要测试的是:Model 和 View 是否被正确地调用。不需要关注数据。(不过如果你的 Presenter 做了数据转换工作,那么还是要进行相应的测试的)

还要测试的一个点是:Presenter 如何处理 Model 的错误,使用 RxJava 我们可以以在 getUserProfile 中返回一个 Observable.error(Throwable()) 的方式来进行。

译注:数据返回有两类:callback / event ,此例中用 RxJava Observable 本质上就是 Event 的方式。callback 的方式要做些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ProfilePresenterTest {
  @Mock
  lateinit var interactor:ProfileInteractor
  @Mock
  lateinit var view:ProfileView

  @Before
  fun setUp() {
    MockitoAnnotations.initMocks(this)
    `when`(interactor.getUserProfile()).thenReturn(
        Observable.just(UserProfile())
    )

    presenter = ProfilePresenter(interactor)
    presenter.attachView(view)
  }
  @Test
  fun testDisplayCalled(){
    verify(interactor).getUserProfile()
    verify(view).display(any())
  }
}

小结

  • Mock Model 和 View 以便于验证被 Presenter 正确调用
  • 根据不同场景 Model 返回假数据。甚至你不需要使用 RxJava

关键理念:

  • 需要有可 Mock 的 Model ,如果没有,封装一层。
  • 使用依赖注入框架来 mock presener.不要在 View 里创建 Presenter
  • 不仅测试输出,还要测试交互
  • 测试 View 的生命周期,如果你的 Presenter 与其有依赖 译注:我们正在做的项目就强依赖生命周期
  • 测试View 的可视变化,不仅仅是文本,还应包括背景色等你需修改的部分。
  • 测试 Presenter 如何处理不同的 Model 返回。

以下为需要学习的知识

  • kotlin 默认为 final class ,需手动设置 open ,或引入 allopen
  • Robolectric
This post is licensed under CC BY 4.0 by the author.