• 《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这样的自动模拟测试框架。

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

  • 相关阅读:
    python上selenium的弹框操作
    python中selenuim模块定位方法详解
    python中Selenium模块的安装与简单使用(详细)
    postman测试用例做断言
    postman写测试用例
    python之单元测试及unittest框架的使用
    spring boot mybatis redis缓存
    web插件
    jQuery事件
    C++对象动态内存
  • 原文地址:https://www.cnblogs.com/easy5weikai/p/16351149.html
Copyright © 2020-2023  润新知