android unit test mock框架使用记录 | 您所在的位置:网站首页 › MVP框架是什么 › android unit test mock框架使用记录 |
写在前面
之前上班时,开发一个功能之后,还需要编写测试用例,使用的框架是mock。 为什么防止以后用到时忘了,在这里记录一下。 由于团队没有人使用Espresso进行unit test,所以本人对该框架并不熟悉。想了解该框架的使用,请移步其他文章。 准备unit test概述Mockito框架编写unit 的思路unit test-实例 准备 // 如果需要测试LiveData,务必引入该库 testImplementation "androidx.test:rules:1.2.0" // 用于生成一些测试过程中需要用到的类 testImplementation "org.mockito:mockito-core:3.0.0" testImplementation "org.mockito:mockito-inline:2.21.0"如果某些网络请求需要依赖json文件模拟获取请求结果,可以使用下面这种方式加载json文件。 // 注意:这是unit test所需的代码,所以最好把该文件创建在unit test的包下面 object FileLoader { fun loadFile(fileName: String): String { val inputStream = Thread.currentThread().contextClassLoader!!.getResourceAsStream(fileName) return inputStream.readBytes().decodeToString() } } // 然后在main目录下创建resources文件夹,再在里面创建相应的json文件或其他类型的文件。 // 如 // aaa.json { } // KotlinTest.kt class KotlinTest { @Test fun test(){ val result = FileLoader.loadFile("aaa.json") println(result) } } // 结果 { } 工具不清楚是哪个android studio的版本,反正右击test目录看看有没有这个就行了,没有就更新android studio的版本。 这个工具的作用是:可以查看当前测试的覆盖率。 我所在的公司是对覆盖率有要求的,虽然不是以该工具为准,但该工具可以辅助我们在开发的时候确保哪些类和方法有准确被测试,哪些没有,从而了解代码要怎么修改才能让覆盖率提升。 在test包或者KotlinTest上面鼠标右键,选择上面的test with Coverage,运行之后可能会出现这样一个弹窗。 有时android studio就会报这种代码出现异常,但这样的代码怎么可能会出现异常,显然是有问题的。所以这个时候就可以使用coverage来辅助检测,看看unit test运行到那里就没有继续运行了,从而找出问题代码。 unit test概述 在编写测试用例之前,需要先明确为什么要编写unit test,如果没有找到一个编写unit test的目的,那编写unit test就会变成一个应付上级的任务。在百度大概查了一下,感觉这段话比较好的说明了unit test的目的:单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。 在20年参加过JetBrains举办的一次网络研讨会 ,里面就有一小段的时间在讨论unit test的作用。我觉得里面说得非常对,大概意思是:在重构之前最好先编写unit test,目的是在重构过程中可以不断地通过unit test去验证代码原有的逻辑是否被破坏。后面我还记得原有的bug在重构之后也必须原封不动的保留下来,是不是在这个视频说的就忘了,反正记得看过/听过这样一句话。 结合这两句话,我觉得可以做一个总结:可以使用unit test保证代码在重构前后原有的逻辑不被破坏。而后面的bug也必须保留,我个人认为:这也是原有的逻辑不被破坏的一个要求之一。因为重构的目的是修改代码结构/架构,而不是改bug。既然是修改代码结构,那就不能涉及到逻辑层面的修改。逻辑层面的修改应该在做其他事情的时候去做,而不是在重构的过程去做。 当然了,编写unit test并不是为了应对代码重构,而是为了验证自己的想法是否准确。当完成一个功能之后,顺手编写unit test对自己的代码逻辑进行验证并且验证通过,这个时候心里就对自己编写的代码有底了。所以时间允许的情况下,我个人认为编写unit test还是有必要的。我就有过在编写unit testd的时候才发现代码有一个不明显的bug的情况。不过如果业务经常变动,就暂时别写unit test了,等业务稳定下来再去写。因为频繁变动带来的是需要频繁修改unit test的代码,如果项目里面还配置了jenkin,导致jenkin动不动就提示unit test测试失败,就不好玩了。 Mockito框架 为什么需要Mock在编写unit test过程中,很多对象并不能轻易获取或者没办法直接构造(如: android.content.Context),而当我们想要创建这些对象的时候,会发现需要导入很多类才能将该类创建出来,这个时候就可以使用Mock将该对象创建出来。比如: 如果需要一个TextView对象,这个时候可以直接new,但new的时候需要一个Context对象,然而创建一个Context又需要很多类。最后会发现,光new一个对象就会浪费大量时间。而如果使用Mock,则可以: val textView = Mockito.mock(TextView::class.java) println(textView) / /或者是 class MockTest { @Mock private lateinit var textView: TextView @Test fun test() { MockitoAnnotations.initMocks(this) println(textView) } } // 这两种方法都是可以的,具体想怎么使用就看自己的需要吧。可以看到,使用Mock就可以如此简单的创建一个TextView,连Context都不需要了。 再看看Mock的其他用法,因为Mock框架非常强大,可以解决编写unit test大部分的问题。 Mock常见用法 verify方法该方法的作用是:测试指定方法是否被调用,先看一下方法前面及文档。
上面的图片可以看到,verify实际上是调用另一个方法,并传入了times(1)这个参数,看看这个是怎么做的。 thenReturn和thenAnswer 当在调用一个Mock对象的方法的时候,需要指定某个返回值,就可以使用这2个方法。 这2个方法的区别是 thenReturn:没办法获取到调用的参数值,适合那些无需根据参数内容返回相应数据的方法。thenAnswer:可以获取到调用方法信息、调用对象、调用参数等信息,适合那些需要根据调用的数据返回相应的数据的方法。
什么意思?假设调用的某个方法里面有100行代码,mock出来的对象是不会执行这100行代码的,而spy出来的对象则会都执行。最终影响到测试的覆盖率。而且由于mock不会执行本来的代码,导致该方法的代码逻辑不会执行。比如通过mock设置TextView里面的textSize,再调用verify方法检测TextView里面的textSize,结果肯定是不通过的,因为mock根本不会执行TextView里面的逻辑。 可以正常运行的情况
回到unit test-实例 在要测试的类类名点击右键就可以比较方便地创建一个Test类。 回到unit test-实例 Presenter主要是对业务进行测试,测试Presenter里面的业务代码的执行结果是否符合预期。一旦发现不符合预期,则有可能编写的测试代码有问题,亦或是业务代码本身就有问题,只是没有被测出来等等。Presenter的测试代码尽量不要涉及UI测试,否则会导致整个测试类看起来非常乱。既包含业务测试,又包含UI测试。 LoginSuccessRequesterMock object LoginSuccessRequesterMock : ILoginContact.ILoginRequester { override fun login(userName: String, password: String, loginListener: loginListener) { loginListener(LoginRequester.REQUEST_SUCCESS, LoginModel(userName, password)) } }LoginFailedRequesterMock object LoginFailedRequesterMock : ILoginContact.ILoginRequester { override fun login(userName: String, password: String, loginListener: loginListener) { loginListener(LoginRequester.REQUEST_FAILED, null) } }LoginPresenterTest class LoginPresenterTest { // 这里的Presenter只有一个测试方法,所以不想要创建一个成员变量也是没问题的 // 但考虑到大部分Presenter都至少不止一个方法,所以建议Presenter都使用成员变量 private lateinit var presenter: LoginPresenter private lateinit var view: ILoginContact.ILoginView @Before fun setUp() { // 使用spy的目的是,为了方便测试Presenter里面的某些方法是否被调用 presenter = Mockito.spy(LoginPresenter) // 由于View在这里只是负责测试逻辑是否正确,所以完全可以使用mock,无需使用spy或是new view = Mockito.mock(ILoginContact.ILoginView::class.java) presenter.addView(view) } @Test fun login() { // 方法的参数建议单独创建,方便测试 val userName = "username" val password = "password" // 由于在Presenter里面将网络请求叫给了Requester去做,所以在编写unit test的时候 // 就可以更方便地控制请求的结果,只需要设置了不同的Requester就行了,无需修改Presenter里面的代码 val loginSuccessRequester = LoginSuccessRequesterMock val loginFailedRequester = LoginFailedRequesterMock // 设置请求成功的时候 // 可以看到在LoginPresenter的Requester是:ILoginRequester,所以这种情况下就可以自由切换Requester,而不是只能LoginRequester的实现类 presenter.requester = loginSuccessRequester presenter.login(userName, password) // 测试login方法是否被调用 Mockito.verify(presenter).login(userName, password) // 测试View是否调用了showLoading Mockito.verify(view).showLoading(RequestType.REQUEST_LOGIN) // 测试View是否调用了hideLoading Mockito.verify(view).hideLoading(RequestType.REQUEST_LOGIN) // 测试loginModel是否相等 Assert.assertEquals(presenter.loginModel, LoginModel(userName, password)) // 测试View是否调用了onLoginSuccess // 这里需要注意的是:由于这里的LoginModel使用的是data class,所以会自动override toString方法 // 并且mock额比较方式就是使用toString,所以如果发现明明代码没问题,但却测试没办法通过 // 就可以试试手动override toString方法 Mockito.verify(view).onLoginSuccess(LoginModel(userName, password)) // 设置请求失败的时候 presenter.requester = loginFailedRequester presenter.login(userName, password) // 这里,由于上面调用了1次,所以这里就变成了调用了2次,所以这种情况下就可以需要使用,VerificationMode // 要使用times还是atLeast就看业务需要和个人习惯吧 Mockito.verify(presenter,Mockito.atLeast(1)).login(userName, password) Mockito.verify(view,Mockito.atLeast(1)).showLoading(RequestType.REQUEST_LOGIN) Mockito.verify(view,Mockito.atLeast(1)).hideLoading(RequestType.REQUEST_LOGIN) //由于请求失败,所以会调用showNetError方法,所以这里需要测试 Mockito.verify(view).showNetError(RequestType.REQUEST_LOGIN) } @After fun tearDown() { presenter.removeView(view) } } View测试回到unit test-实例 View测试只是测试调用特定方法之后,相应的View有没有变成和预期一样的结果。所以这里没必要编写过多的业务代码,尽可能地少编写业务代码辅助测试。所以View层的测试代码就会变得比较简单。只是会比较麻烦,因为View的很多代码对于java来说是不存在的,所以需要手动mock出来。 LoginActivityTest class LoginActivityTest { private lateinit var view: LoginActivity @Before fun setUp() { view = Mockito.spy(LoginActivity::class.java) } @Test fun testPresenter() { // 为了提升覆盖率, 所以有一些看起来好像不相关的代码,也最好形式上调用一下 view.initPresenter() val inViewPresenter = view::class.java.getDeclaredField("presenter").let { it.isAccessible = true it.get(view) as LoginPresenter } Assert.assertTrue(inViewPresenter.views.contains(view)) view.deinitPresenter() Assert.assertFalse(inViewPresenter.views.contains(view)) } @Test fun initView() { // 这里直接使用了view的id,这是kotlin的一个扩展库(虽然最新已废弃) // 使用过show kotlin bytecode的朋友应该都知道,实际上最终编译出来的代码是使用 _$_findViewCache // 这里一个变量来存储,所以为了保证在调用initView的时候不会出现空指针异常,需要在调用之前将所需 // 的view设置进去 val button = Mockito.mock(Button::class.java) view::class.java.getDeclaredField("_\$_findViewCache").also { it.isAccessible = true it.set(view, hashMapOf(R.id.login_btn to button)) } view.initView() // 验证Button是否调用了setOnClickListener,由于在initView里面,使用的是匿名对象 // 所以如果直接编写:Mockito.verify(button).setOnClickListener{},会测试失败 // 这个时候就可以使用Mockito.any(View.OnClickListener::class.java)来充当View.OnClickListener对象 Mockito.verify(button).setOnClickListener(Mockito.any(View.OnClickListener::class.java)) // 如果View是Activity,并且使用的是findViewById的方式,则可以这样做 // val delegate = Mockito.mock(AppCompatDelegate::class.java) // Mockito.`when`(view.delegate).thenReturn(delegate) // Mockito.`when`(delegate.findViewById(R.id.login_btn)).thenReturn(Mockito.mock(Button::class.java)) // 授人以鱼不如授人以渔,我所在的团队中,View并不是使用Activity的方式 // 上面这段代码其实是看了Activity的findViewById之后写出来的,所以如果View是以其他形式出现 // 可以去看该对象的源码,从而找到解决出异常的办法 } @Test fun showLoading() { // 这里的AppCompatImageView是随便写的,只是为了让界面有一个View测试 // 由于java api并不存在AppCompatImageView,所以只能mock出来,这里用spy的话,会出现一堆问题 // 所以还是用mock比较方便 val loadingView = Mockito.mock(AppCompatImageView::class.java) // 说实话,每次都要写这样一段代码,其实挺烦的,建议实际开发的时候抽成一个方法 // 实际开发中,我就编写了一个工具,这样就可以减少重复代码的编写了 view::class.java.getDeclaredField("_\$_findViewCache").also { it.isAccessible = true it.set(view, hashMapOf(R.id.loadingview to loadingView)) } // 调用view.showLoading(RequestType.REQUEST_LOGIN),实际上会调用:loadingview.visibility = View.VISIBLE view.showLoading(RequestType.REQUEST_LOGIN) // 测试是否调用了visibility = View.VISIBLE Mockito.verify(loadingView).visibility = View.VISIBLE } @Test fun hideLoading() { val loadingView = Mockito.mock(AppCompatImageView::class.java) view::class.java.getDeclaredField("_\$_findViewCache").also { it.isAccessible = true it.set(view, hashMapOf(R.id.loadingview to loadingView)) } // 如果在某些情况下,没办法使用类似上面的verify方式进行验证 // 则可以使用这种方式辅助验证 var visibility = -1 Mockito.`when`(loadingView.setVisibility(View.GONE)).thenAnswer { if (it.arguments[0].equals(View.GONE)) { visibility = View.GONE } } view.hideLoading(RequestType.REQUEST_LOGIN) Assert.assertEquals(visibility, View.GONE) } @Test fun onLoginSuccess() { val textView = Mockito.mock(AppCompatTextView::class.java) view::class.java.getDeclaredField("_\$_findViewCache").also { it.isAccessible = true it.set(view, hashMapOf(R.id.result_tv to textView)) } val model = LoginModel("u", "t") view.onLoginSuccess(model) Mockito.verify(textView).text = model.userName } } ViewModel测试回到unit test-实例 MVP和MVVM一个比较大的区别是:MVVM使用了ViewModel对View进行更新,所以这里单独将ViewModel拿出来说明。 首先提醒一下,由于所在团队中不是使用databinding+viewmodel,所以不清楚如果代码这样写,test要怎么做,所以不提供相关示例。 TestViewModelActivity class TestViewModelActivity : AppCompatActivity() { private lateinit var viewModel: TestViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_test_view_model) viewModel = initViewModel() bindViewModel() setData() } fun initViewModel(): TestViewModel = ViewModelProvider(this)[TestViewModel::class.java] fun bindViewModel() { viewModel.text.observe(this) { textView.text = it } } fun setData() { viewModel.text.value = "test test test" } }TestViewModelActivityTest class TestViewModelActivityTest { private lateinit var activity: TestViewModelActivity // 可以尝试把这行代码去掉,会发现测试setData方法的时候出现空指针异常 // 因为LiveData最终通知数据更新离不开Handler,但这是Android的,所以google用了这个来解决这个问题 @get:Rule val rule: TestRule = InstantTaskExecutorRule() @Before fun setUp() { activity = Mockito.spy(TestViewModelActivity()) } @Test fun setData() { val viewModel = TestViewModel() // 塞一个ViewModel进去 activity::class.java.getDeclaredField("viewModel").also { it.isAccessible = true it.set(activity, viewModel) } activity.bindViewModel() // 比较遗憾的是:调用这之后并不会执行Observer里面的代码 activity.setData() // 但可以通过观察viewModel里面的变量的value来确实测试有没有通过 Assert.assertEquals(viewModel.text.value, "test test test") } } |
CopyRight 2018-2019 实验室设备网 版权所有 |