音频播放与单元测试
音频播放与单元测试:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。
学习目标
- 能沿“音频播放与单元测试”的用户事件解释Android组件、状态所有者、线程与销毁边界。
- 能围绕“掌握 MediaPlayer 音频播放和 JUnit 单元测试——用 Mockito 隔离依赖,写出可测试的代码。”改出一个可运行结果,并用前后状态而非组件数量验收。
- 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
- 能用构建指纹、用户操作、状态快照、原始日志和行为断言独立重放结论,并标明第四版机制与现代targetSdk政策的边界。
你改了 3 行代码,怎么确认没有破坏其他功能?
每次发版前手动点一遍所有页面?App 有 100 个页面怎么办?自动化测试是你最好的 QA。这章教你两个关键技术:MediaPlayer(最常用的音频播放)和 JUnit 单元测试(保证代码质量)。
MediaPlayer 音频播放
class AudioPlayer(context: Context) {
private var mediaPlayer: MediaPlayer? = null
fun play(resId: Int) {
release() // 先释放旧的
mediaPlayer = MediaPlayer.create(context, resId).apply {
setOnCompletionListener { release() }
start()
}
}
fun pause() = mediaPlayer?.pause()
fun stop() = mediaPlayer?.apply { stop(); prepareAsync() }
fun release() {
mediaPlayer?.release()
mediaPlayer = null
}
}关键规则:用完必须 release()——MediaPlayer 持有系统音频资源,不释放会导致资源泄漏甚至其他 App 无声音。
对于短促音效(按钮点击声、通知铃声),用 SoundPool 替代 MediaPlayer:
val soundPool = SoundPool.Builder().setMaxStreams(3).build()
val soundId = soundPool.load(context, R.raw.click, 1)
soundPool.play(soundId, 1.0f, 1.0f, 1, 0, 1.0f)SoundPool 预加载音频到内存,低延迟播放短音效——MediaPlayer 适合长音频(音乐/播客)。
① create
MediaPlayer.create(context, resId) 是最简捷的创建方式——内部已调用
setDataSource + prepare(),创建后即可 start()。这也意味着它在
create() 阶段就可能因为资源不存在或格式不支持而抛异常。
单元测试基础
JUnit 4 结构
class CalculatorTest {
private lateinit var calculator: Calculator
@Before
fun setUp() {
calculator = Calculator()
}
@Test
fun `addition should return sum of two numbers`() {
val result = calculator.add(2, 3)
assertEquals(5, result)
}
@Test(expected = IllegalArgumentException::class)
fun `division by zero should throw`() {
calculator.divide(10, 0)
}
}Mockito 模拟依赖
@Test
fun `loadData success should update LiveData`() {
// 模拟 Repository
val mockRepo = mock(Repository::class.java)
`when`(mockRepo.fetchData()).thenReturn(listOf("a", "b"))
val viewModel = MainViewModel(mockRepo)
viewModel.loadData()
// 验证 LiveData 结果
assertEquals(listOf("a", "b"), viewModel.data.getOrAwaitValue())
}Mockito 的 mock() 创建一个假对象,when().thenReturn() 控制它返回什么——让你在测试中隔离外部依赖(数据库、网络),只测 ViewModel 自身逻辑。
容易踩的坑
小结
- MediaPlayer 适合长音频(音乐/播客),SoundPool 适合短音效(点击声)
- MediaPlayer 用完必须 release(),播放新音频前先释放旧实例
- JUnit:@Test 标记测试方法,@Before 准备环境,assertEquals 验证结果
- Mockito 模拟依赖 → 隔离外部因素,只测被测代码本身
- 单元测试测纯逻辑(ViewModel/Repository),Instrumentation 测试测 UI 交互
练习
问题 1:“第20章 Unit Testing and Audio Playback”覆盖哪些正式节点和项目主线?
问题 2:怎样建立本页最小可执行实验?
问题 3:为什么只在正常点击路径运行不能证明完成?
问题 4:怎样设计能推翻当前实现的反例?
问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?
问题 6:本页达到独立交接标准需要什么?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- SoundPool
短音效播放器——预加载音频到内存,低延迟播放(<10ms)。适合按钮点击声、通知铃声、游戏音效。不适合长音频(音乐用 MediaPlayer)。
- Mockito
Java/Kotlin 的 mock 测试框架。
mock()创建假对象,when().thenReturn()控制行为,verify()验证方法是否被调用。隔离依赖、专注测试被测代码。
“音频播放与单元测试”不使用未获授权的纸书正文;InformIT出版信息与授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。
为什么“音频播放与单元测试”必须回到可观察状态
“音频播放与单元测试”的学习结果不是记住类名,而是能预测“掌握 MediaPlayer 音频播放和 JUnit 单元测试——用 Mockito 隔离依赖,写出可测试的代码。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。
第四版机制逐项深读
20. Unit Testing and Audio Playback
在“音频播放与单元测试”中,“20. Unit Testing and Audio Playback”分离音频资源加载、播放ID、释放和可测试回调;单元测试验证状态与协作,设备测试再验证真实SoundPool时序和旋转后不重复播放。
Creating a SoundPool
在“音频播放与单元测试”中,“Creating a SoundPool”分离音频资源加载、播放ID、释放和可测试回调;单元测试验证状态与协作,设备测试再验证真实SoundPool时序和旋转后不重复播放。
Accessing Assets
在“音频播放与单元测试”中,“Accessing Assets”若依赖第四版时期API,应分开说明原书机制与现代平台政策,并用Android官方文档核对迁移边界。
Loading Sounds
在“音频播放与单元测试”中,验证“Loading Sounds”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。
Playing Sounds
在“音频播放与单元测试”中,“Playing Sounds”服务于“掌握 MediaPlayer 音频播放和 JUnit 单元测试——用 Mockito 隔离依赖,写出可测试的代码。”;解释要落到用户事件、Android所有者、状态变化、线程和可观察结果,并给出一个会推翻实现的反例。
Test Dependencies
在“音频播放与单元测试”中,“Test Dependencies”若依赖第四版时期API,应分开说明原书机制与现代平台政策,并用Android官方文档核对迁移边界。
Creating a Test Class
在“音频播放与单元测试”中,“Creating a Test Class”分离音频资源加载、播放ID、释放和可测试回调;单元测试验证状态与协作,设备测试再验证真实SoundPool时序和旋转后不重复播放。
Setting Up Your Test
在“音频播放与单元测试”中,验证“Setting Up Your Test”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。
Writing Tests
在“音频播放与单元测试”中,“Writing Tests”若依赖第四版时期API,应分开说明原书机制与现代平台政策,并用Android官方文档核对迁移边界。
Data Binding Callbacks
在“音频播放与单元测试”中,分析“Data Binding Callbacks”要画出事件进入、状态归约、数据源写入和UI重绘四条边,并指出配置变化时谁被重建。
Unloading Sounds
在“音频播放与单元测试”中,分析“Unloading Sounds”先冻结设备API、targetSdk和输入,再沿回调与数据流寻找首个状态分叉,不能只描述最终页面。
For the More Curious: Integration Testing
在“音频播放与单元测试”中,“For the More Curious: Integration Testing”分离音频资源加载、播放ID、释放和可测试回调;单元测试验证状态与协作,设备测试再验证真实SoundPool时序和旋转后不重复播放。
For the More Curious: Mocks and Testing
在“音频播放与单元测试”中,“For the More Curious: Mocks and Testing”分离音频资源加载、播放ID、释放和可测试回调;单元测试验证状态与协作,设备测试再验证真实SoundPool时序和旋转后不重复播放。
Challenge: Playback Speed Control
在“音频播放与单元测试”中,“Challenge: Playback Speed Control”分离音频资源加载、播放ID、释放和可测试回调;单元测试验证状态与协作,设备测试再验证真实SoundPool时序和旋转后不重复播放。
Challenge: Play Sound Across Rotation
在“音频播放与单元测试”中,“Challenge: Play Sound Across Rotation”用于推翻“掌握 MediaPlayer 音频播放和 JUnit 单元测试——用 Mockito 隔离依赖,写出可测试的代码。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。
“音频播放与单元测试”验收回顾
“音频播放与单元测试”只有在构建指纹、用户操作、状态快照、原始日志和行为断言能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。
章专属可重放状态实验
先预测“加载资产、点击播放、旋转、重复点击与退出”发生后,BeatBox、SoundPool 与测试替身应怎样改变sound ID、加载完成、播放请求、生命周期和释放状态;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。
实验一:所有者—状态—结果合同
选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。
Owner · state · observable result
音频播放与单元测试:状态合同
把 SoundPool 加载、播放、释放与可测试回调分离
验证场景
第四版正式目录节点
audio-unit-testing · 正常任务
20. Unit Testing and Audio Playback:固定 SDK、设备配置和初始状态,触发“加载资产、点击播放、旋转、重复点击与退出”
冻结入口:20. Unit Testing and Audio Playback
记录BeatBox、SoundPool 与测试替身的初始sound ID、加载完成、播放请求、生命周期和释放状态
观察:单元测试、load 回调、stream ID、实例计数和释放日志中的“20. Unit Testing and Audio Playback”轨迹
预期:由BeatBox、SoundPool 与测试替身提交sound ID、加载完成、播放请求、生命周期和释放状态,并持续满足“未加载完成不播放,释放后不再接收回调,同一事件不重复发声”
实验二:事件与生命周期轨迹
沿五次转换逐步执行“加载资产、点击播放、旋转、重复点击与退出”。每一步只允许BeatBox、SoundPool 与测试替身按职责提交状态,并持续核对“未加载完成不播放,释放后不再接收回调,同一事件不重复发声”。
Deterministic event replay
音频播放与单元测试:事件轨迹
不变量:未加载完成不播放,释放后不再接收回调,同一事件不重复发声
交付证据:单元测试、load 回调、stream ID、实例计数和释放日志
实验三:章专属反例与同输入恢复
注入“旋转后两个 SoundPool 同时存活,同一次点击播放两遍”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有单元测试、load 回调、stream ID、实例计数和释放日志一起恢复才算修复。
Fault · cancel · restore
音频播放与单元测试:反例与恢复
故障:旋转后两个 SoundPool 同时存活,同一次点击播放两遍
第 1 次使用相同 SDK、设备配置、初始状态与用户事件
保持正常输入不变,仅注入“旋转后两个 SoundPool 同时存活,同一次点击播放两遍”
未加载完成不播放,释放后不再接收回调,同一事件不重复发声
单元测试、load 回调、stream ID、实例计数和释放日志