音频播放与单元测试

音频播放与单元测试:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。

学习目标

  • 能沿“音频播放与单元测试”的用户事件解释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 适合长音频(音乐/播客)。

分步1 / 5

① 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 自身逻辑。

测试金字塔:大量快测压在底层,少量贵测放在顶层底宽顶窄 —— 越往下越多、越快、越廉;越往上越少、越慢、越贵UI 测试Espresso · 真机 / 模拟器少 / 慢 / 贵集成测试Robolectric · 组件间协作适中单元测试JUnit + Mockito / MockK · JVM 上跑多 / 快 / 廉 — 测 ViewModel / 纯逻辑少 / 慢 / 贵多 / 快 / 廉越往下越多、越快、越廉底层单元测试数量最多、毫秒级、不花钱;顶层 UI 测试数量最少、跑得慢、开销大。用依赖注入 / 接口替身隔离外部依赖把 SoundPool 包成接口,测试时塞 test double(mock);纯逻辑不碰系统资源,才进得了金字塔底层快测。本章的做法把音频播放逻辑抽到一个可测的类里 ——依赖接口而非具体 MediaPlayer / SoundPool,即可单测。
测试金字塔:把大量又快又廉的单元测试压在底层(JUnit + Mockito 测 ViewModel / 纯逻辑),少量又慢又贵的 UI 测试(Espresso)放在顶层。想测得动,先把 音频播放逻辑抽成依赖接口的可测类,再用 test double 隔离 SoundPool 这类系统资源。

容易踩的坑

小结

  • 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名称掩盖旧行为。

← 上一页:数据绑定与MVVM · 下一页:样式与主题 →

章专属可重放状态实验

先预测“加载资产、点击播放、旋转、重复点击与退出”发生后,BeatBox、SoundPool 与测试替身应怎样改变sound ID、加载完成、播放请求、生命周期和释放状态;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。

实验一:所有者—状态—结果合同

选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。

Owner · state · observable result

音频播放与单元测试:状态合同

把 SoundPool 加载、播放、释放与可测试回调分离

验证场景

第四版正式目录节点

audio-unit-testing · 正常任务

20. Unit Testing and Audio Playback固定 SDK、设备配置和初始状态,触发“加载资产、点击播放、旋转、重复点击与退出”

状态所有者BeatBox、SoundPool 与测试替身
受控状态sound ID、加载完成、播放请求、生命周期和释放状态
触发事件加载资产、点击播放、旋转、重复点击与退出

冻结入口: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

音频播放与单元测试:事件轨迹

选择一次状态转换1 / 5

不变量:未加载完成不播放,释放后不再接收回调,同一事件不重复发声

交付证据:单元测试、load 回调、stream ID、实例计数和释放日志

实验三:章专属反例与同输入恢复

注入“旋转后两个 SoundPool 同时存活,同一次点击播放两遍”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有单元测试、load 回调、stream ID、实例计数和释放日志一起恢复才算修复。

Fault · cancel · restore

音频播放与单元测试:反例与恢复

故障:旋转后两个 SoundPool 同时存活,同一次点击播放两遍

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“旋转后两个 SoundPool 同时存活,同一次点击播放两遍”

3. 检查所有者一致

未加载完成不播放,释放后不再接收回调,同一事件不重复发声

4. 核对结果一致

单元测试、load 回调、stream ID、实例计数和释放日志

讨论

评论区加载中…