《Android 编程权威指南》学习笔记 : 第20章 音频播放与单元测试

测试依赖

添加测试所需要的依赖:

  • 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)
        }
    }
}

同时得修改两个地方的代码:

  1. MainActivity中 SoundHolder:
    代码清单:MainActivity.kt
    private inner class SoundHolder(private val binding: ListItemSoundBinding) :
        RecyclerView.ViewHolder(binding.root) {

            init {
                binding.viewModel = SoundViewModel(beatBox)
            }
       ...
    }
  1. 测试代码:
    代码清单: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这样的自动模拟测试框架。

不管哪种情况,基本原则都一样:模拟对象的效用不应超出受测组件的边界。应着重关注测试范围,防止测试越界。当然,如果受测组件自己失灵,那就另当别论了

posted @ 2022-06-07 11:25  easy5  阅读(147)  评论(0)    收藏  举报