测试依赖
添加测试所需要的依赖:
- JUnit:默认已经添加
- Mockito: 模拟对象
打开菜单【File】,选择【Project Structure】,在【Dependenices > Modules > app】,点击【+】按钮,选择【Library Dependenices】
在搜索框输入:org.mockito,点击 Search, 选择类库
- mockito-core
- mockito-inline
然后在 Step 2中选择【testImplementation】
查看:app/build.gradle
testImplementation 'org.mockito:mockito-core:4.6.1'
testImplementation 'org.mockito:mockito-inline:4.6.1'
记得 Sync now
testImplementation作用范围表示,这两个依赖项只包括在应用的测试编译里。这样就能避免在APK包里捎带上无用代码库了。
你用来创建和配置模拟对象的函数都在mockito-core里了。
而mockito-inline是方便Mockito搭配Kotlin使用的特殊依赖。
在Kotlin中,所有的类都是final的。也就是说,要想继承这些类,就得用上open修饰符。不幸的是,Mockito主要靠继承来模拟测试类。这样一来,如果Mockito想模拟Kotlin类,就做不到开箱即用了。mockito-inline依赖的作用就是绕开Kotlin的继承限制,不用修改源文件,就能让Mockito模拟Kotlin的那些final类和函数。
创建测试类
JUnit是最常用的Android单元测试框架,能和Android Studio无缝整合。要用它测试,首先要创建一个用作JUnit测试的测试类。打开SoundViewModel.kt文件,使用Command+Shift+T(或Ctrl+Shift+T)组合键。Android Studio会尝试寻找这个类关联的测试类。如果找不到,它就会提示新建
最后一步是选择创建哪种测试类,或者说选择哪个测试目录存放测试类(androidTest和test)。在androidTest目录下的都是整合测试类。
这里,我们进行的是单元测试,故选择 test目录测试
点击【OK】按钮,自动生成测试类,如下图所示
修改测试类并运行
修改下SoundViewModel.kt
class SoundViewModel: BaseObservable() {
// val title: MutableLiveData<String?> = MutableLiveData()
//
// var sound: Sound? = null
// set(sound) {
// field = sound
// title.postValue(sound?.name) // 通知布局,数据更新了
// }
var sound: Sound? = null
set(sound) {
field = sound
notifyChange()
}
@get:Bindable
val title: String?
get() = sound?.name
}
修改测试类SoundViewModelTest.kt
class SoundViewModelTest {
private lateinit var sound: Sound
private lateinit var subject: SoundViewModel
@Before
fun setUp() {
sound = Sound("assetPath")
subject = SoundViewModel()
subject.sound = sound
}
@Test
fun exposesSoundNameAsTitle() {
assertTrue(subject.title.equals(sound.name))
}
}
- 以@Before注解的包含公共代码的函数会在所有测试之前运行一次。按照约定,所有单元测试类都要有一个以@Before注解的setUp()函数。
为了运行测试,右键单击SoundViewModelTest类名,然后选择Run 'SoundViewModelTest'。随后,Android Studio的底部窗口会显示测试结果
通过测试:
测试对象交互
你可以在测试里创建一个BeatBox对象,然后把它传给视图模型的构造函数。但是这样做会带来一个问题:如果BeatBox有问题,那么在SoundViewModel里使用BeatBox的测试也会出问题。事与愿违,SoundViewModel的单元测试只有在SoundViewModel有问题时才会失败。
换句话说,我们只想测试SoundViewModel的行为表现。至于它和其他类的交互应该隔离开来。这才是单元测试的关键原则。
解决办法是使用模拟BeatBox。这个模拟对象是BeatBox的子类,有和BeatBox一样的功能,但不做任何事。这样一来,测试SoundViewModel时,我们假定它能正确使用BeatBox。
要使用Mockito创建模拟对象,调用mock(Class)静态函数,传入要模拟的类就可以了。
修改 SoundViewModel.kt
class SoundViewModel: BaseObservable() {
...
fun onButtonClicked() {
}
}
按组合键:Ctrl+Shift+T,自动回到对应的测试类 SoundViewModelTest
添加测试方法
代码清单:app/java/com.example.beatbox.test/SoundViewModelTest.kt
class SoundViewModelTest {
private lateinit var beatBox: BeatBox
...
@Before
fun setUp() {
beatBox = mock(BeatBox::class.java) // 模拟板的BeatBox
...
}
@Test
fun callBeatBoxPlayOnButtonClicked() {
subject.onButtonClicked()
// 验证beatBox的play(sound) 是否被调用了
verify(beatBox).play(sound)
}
调用verify(beatBox)函数就是说:“我要验证beatBox对象的某个函数是否调用了。”紧跟的beatBox.play(sound)函数是说:“验证这个函数是这样调用的。”合起来就是说:“验证以sound作为参数,调用了beatBox对象的play(...)函数。”
运行结果:失败
因为类SoundViewModel甚至都还没有 BeatBox的实例对象,怎么可能调用其对象方法,
修改 SoundViewModel.kt,
- 添加一个属性 beatBox: BeatBox
- 并修改 onButtonClicked 方法
class SoundViewModel(private val beatBox: BeatBox): BaseObservable() {
...
fun onButtonClicked() {
sound?.let {
beatBox.play(it)
}
}
}
同时得修改两个地方的代码:
- MainActivity中 SoundHolder:
代码清单:MainActivity.kt
private inner class SoundHolder(private val binding: ListItemSoundBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.viewModel = SoundViewModel(beatBox)
}
...
}
- 测试代码:
代码清单:MainActivity.kt
class SoundViewModelTest {
...
@Before
fun setUp() {
...
subject = SoundViewModel(beatBox)
}
再次运行测试,可以使用测试方法旁边的运行按钮运行测试,如下图所示:
运行结果:测试通过,如下图所示:
数据绑定回调
按钮要响应事件还差最后一步:关联按钮对象和onButtonClicked()函数。
和前面使用数据绑定关联数据和UI一样,你也可以使用lambda表达式,让数据绑定帮忙关联按钮和点击监听器(
在布局文件里,添加数据绑定lambda表达式,让按钮对象和SoundViewModel.onButtonClicked()函数关联起来。
代码清单:res/layout/list_item_sound.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.beatbox.SoundViewModel" />
</data>
<Button
android:layout_width="match_parent"
android:layout_height="120dp"
android:text="@{viewModel.title}"
android:onClick="@{() -> viewModel.onButtonClicked()}"
tools:text="Sound Name"/>
</layout>
- android:onClick="@{() -> viewModel.onButtonClicked()}" : 将UI上的Button的点击事件与viewModel的onButtonClicked()方法进行关联
运行BeatBox应用,点击按钮。你会听到各种吓人的喊叫声。
释放音频
BeatBox应用可用了,但别忘了做善后工作。音频播放完毕,应调用SoundPool.release()函数释放SoundPool,
然后在在MainActivity中,完成BeatBox对象的释放。
代码清单:BeatBox.kt
class BeatBox(private val assets: AssetManager) {
...
/**
* 释放音频
*/
fun release() {
soundPool.release()
}
}
代码清单:MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var beatBox: BeatBox
...
override fun onDestroy() {
super.onDestroy()
beatBox.release()
}
再次运行应用,确认添加release()函数后,应用工作正常。尝试播放长一点儿的声音,然后旋转设备或点击回退键,声音播放应该会停止.
深入学习:模拟对象与测试
在整合测试场景中,模拟对象显然不能用来隔离应用,相反,我们用它把应用和可能的外部交互对象隔离开来,比如提供Web service假数据和假反馈。如果是在BeatBox应用里,你很可能就要提供模拟SoundPool,让它告诉你某个声音文件何时播放。显然,相比常见的行为模拟,这种模拟太重了,而且还要在很多整合测试里共享。这真不如手动写假对象。
所以,做整合测试时,最好避免使用像Mockito这样的自动模拟测试框架。
不管哪种情况,基本原则都一样:模拟对象的效用不应超出受测组件的边界。应着重关注测试范围,防止测试越界。当然,如果受测组件自己失灵,那就另当别论了