Android音频之单元测试
Android 中的 Unit Test(单元测试)是开发过程中非常重要的一部分,用于验证代码的正确性和稳定性。Android 的单元测试可以分为两类:本地单元测试(Local Unit Tests)和仪器化测试(Instrumented Tests)。以下是对 Android 单元测试的系统性介绍:
1. 单元测试的分类
(1)本地单元测试(Local Unit Tests)
- 运行环境:JVM(不需要 Android 设备或模拟器)。
- 适用场景:测试不依赖 Android 框架的纯 Java/Kotlin 代码。
- 工具:JUnit、Mockito 等。
- 目录:
<module>/src/test/
(2)仪器化测试(Instrumented Tests)
- 运行环境:Android 设备或模拟器。
- 适用场景:测试依赖 Android 框架的代码(如 UI、SharedPreferences、数据库等)。
- 工具:AndroidJUnitRunner、Espresso 等。
- 目录:
<module>/src/androidTest/
2. 单元测试的工具和框架
(1)JUnit
- 作用:标准的 Java 单元测试框架。
- 常用注解:
@Test
:标记测试方法。@Before
:在每个测试方法之前运行。@After
:在每个测试方法之后运行。@BeforeClass
:在所有测试方法之前运行(静态方法)。@AfterClass
:在所有测试方法之后运行(静态方法)。@Ignore
:忽略某个测试方法。
(2)Mockito
- 作用:用于创建和管理 mock 对象,模拟依赖项的行为。
- 常用方法:
mock()
:创建一个 mock 对象。when().thenReturn()
:定义 mock 对象的行为。verify()
:验证 mock 对象的方法是否被调用。
(3)AndroidJUnitRunner
- 作用:Android 提供的测试运行器,用于运行仪器化测试。
- 特点:支持在 Android 设备或模拟器上运行测试。
(4)Espresso
- 作用:用于测试 UI 交互。
- 特点:提供简洁的 API 来模拟用户操作(如点击、输入文本等)。
3. 编写单元测试
(1)本地单元测试示例
假设有一个简单的计算器类 Calculator
:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
对应的单元测试类:
import org.junit.Test;
import static org.junit.Assert.*;
public class CalculatorTest {
@Test
public void addition_isCorrect() {
Calculator calculator = new Calculator();
assertEquals(4, calculator.add(2, 2));
}
}
(2)仪器化测试示例
假设有一个依赖 Android 上下文的类 SharedPreferencesHelper
:
public class SharedPreferencesHelper {
private SharedPreferences sharedPreferences;
public SharedPreferencesHelper(Context context) {
sharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);
}
public void saveData(String key, String value) {
sharedPreferences.edit().putString(key, value).apply();
}
public String getData(String key) {
return sharedPreferences.getString(key, "");
}
}
对应的仪器化测试类:
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
@RunWith(AndroidJUnit4.class)
public class SharedPreferencesHelperTest {
@Test
public void testSaveAndGetData() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
SharedPreferencesHelper helper = new SharedPreferencesHelper(context);
helper.saveData("key", "value");
assertEquals("value", helper.getData("key"));
}
}
4. 运行单元测试
(1)在 Android Studio 中运行
- 本地单元测试:右键点击测试类或方法,选择
Run
。 - 仪器化测试:确保设备或模拟器已连接,然后右键点击测试类或方法,选择
Run
。
(2)使用命令行运行
- 本地单元测试:
./gradlew test
- 仪器化测试:
./gradlew connectedAndroidTest
5. 测试覆盖率
测试覆盖率是衡量测试质量的重要指标。可以使用以下工具生成测试覆盖率报告:
(1)JaCoCo
- 作用:生成代码覆盖率报告。
- 配置:
在
build.gradle
中添加:android { testOptions { unitTests.all { jacoco { includeNoLocationClasses = true } } } }
- 生成报告:
./gradlew jacocoTestReport
(2)Emma
- 作用:生成代码覆盖率报告。
- 配置:
在
build.gradle
中添加:android { testOptions { unitTests.all { jacoco { includeNoLocationClasses = true } } } }
6. 最佳实践
(1)测试命名规范
- 测试方法名应清晰描述测试的目的,例如
addition_isCorrect
。 - 使用
@DisplayName
(JUnit 5)或方法名本身来描述测试场景。
(2)测试独立性
- 每个测试方法应该是独立的,不依赖其他测试方法的结果。
- 使用
@Before
和@After
初始化和清理测试环境。
(3)Mock 依赖项
- 使用 Mockito 模拟依赖项,避免测试受外部因素影响。
(4)测试覆盖率
- 尽量覆盖所有代码路径(如条件分支、异常处理等)。
- 定期检查测试覆盖率报告,确保关键代码被测试覆盖。
7. 常见问题
(1)测试运行失败
- 检查测试代码是否正确。
- 确保测试环境配置正确(如设备连接、模拟器启动)。
(2)测试依赖问题
- 确保测试模块依赖了正确的库(如
junit
、mockito
)。 - 对于仪器化测试,确保依赖了
androidx.test
相关库。
(3)测试性能问题
- 避免在测试中执行耗时操作(如网络请求、数据库读写)。
- 使用 mock 对象替代真实依赖。
8. 实践
要编译并测试 android/packages/services/Car/tests/carservice_unit_test/src/com/android/car/audio/CarAudioFocusUnitTest.java
,你需要按照以下步骤操作。这个过程涉及 Android 源码的构建系统和测试框架。
1. 环境准备
在开始之前,确保你已经完成以下准备工作:
- 下载并配置 Android 源码:确保你已经下载了完整的 Android 源码,并配置好了构建环境(如
repo
、lunch
等)。 - 安装依赖:确保你的系统已经安装了构建 Android 所需的工具(如 JDK、Make、Ninja 等)。
- 设备或模拟器:如果是仪器化测试,确保有一个连接的 Android 设备或模拟器。
2. 编译测试模块
(1)进入源码根目录
cd <android-source-root>
(2)设置构建环境
source build/envsetup.sh
(3)选择目标设备
lunch <target>
例如:
lunch aosp_car_x86_64-userdebug
(4)编译测试模块
使用 mmm
命令编译特定的模块:
mmm packages/services/Car/tests/carservice_unit_test
mmm
用于编译指定路径下的模块。- 如果编译成功,会生成测试 APK 文件(如
CarServiceUnitTest.apk
)。
3. 运行测试
(1)安装测试 APK
将生成的测试 APK 安装到设备或模拟器上:
adb install -r $OUT/data/app/CarServiceUnitTest/CarServiceUnitTest.apk
(2)运行测试
使用 adb shell
运行测试:
adb shell am instrument -w -r -e debug false -e class com.android.car.audio.CarAudioFocusUnitTest -e reportDir /sdcard/Android/data/com.android.car.carservice_unittest/files/ com.android.car.carservice_unittest/androidx.test.runner.AndroidJUnitRunner
-e class
:指定要运行的测试类。com.android.car.carservice_unittest
:测试 APK 的包名。
4. 查看测试结果
(1)在终端查看
测试结果会直接输出到终端。如果测试通过,你会看到类似以下的输出:
OK (X tests)
(2)生成测试报告
测试报告会生成在设备的 /sdcard/
目录下。你可以使用以下命令将报告拉取到本地:
adb pull /sdcard/Android/data/com.android.car.carservice_unittest/files/ <local-dir>
5. 调试测试
(1)使用 Logcat 查看日志
在测试运行时,可以使用 logcat
查看详细的日志输出:
adb logcat -s TestRunner
(2)使用 Android Studio 调试
- 将 Android 源码导入 Android Studio。
- 在
CarAudioFocusUnitTest.java
中设置断点。 - 使用 Android Studio 的调试功能运行测试。
6. 常见问题
(1)编译失败
- 检查是否缺少依赖模块。
- 确保
lunch
选择了正确的目标设备。
(2)测试失败
- 检查测试代码是否正确。
- 确保设备或模拟器已正确连接。
(3)测试 APK 未安装
- 确保设备或模拟器有足够的存储空间。
- 检查
adb devices
是否显示设备已连接。
verify方法介绍
verify
是 Mockito 框架中的一个核心方法,用于验证某个对象的特定方法是否被调用,以及调用的次数和参数是否符合预期。它是单元测试中非常重要的工具,尤其是在测试依赖其他对象的方法时。
以下是对 verify
用法的详细介绍:
1. 基本用法
(1)验证方法是否被调用
verify(mockObject).methodName();
- 作用:验证
mockObject
的methodName
方法是否被调用过一次。 - 示例:
List<String> mockedList = mock(List.class); mockedList.add("test"); verify(mockedList).add("test");
(2)验证方法被调用的次数
verify(mockObject, times(n)).methodName();
- 作用:验证
mockObject
的methodName
方法被调用了n
次。 - 示例:
List<String> mockedList = mock(List.class); mockedList.add("test"); mockedList.add("test"); verify(mockedList, times(2)).add("test");
(3)验证方法从未被调用
verify(mockObject, never()).methodName();
- 作用:验证
mockObject
的methodName
方法从未被调用。 - 示例:
List<String> mockedList = mock(List.class); verify(mockedList, never()).add("test");
(4)验证方法至少/至多被调用
verify(mockObject, atLeast(n)).methodName(); // 至少 n 次
verify(mockObject, atMost(n)).methodName(); // 至多 n 次
- 示例:
List<String> mockedList = mock(List.class); mockedList.add("test"); mockedList.add("test"); verify(mockedList, atLeast(1)).add("test"); // 至少调用 1 次 verify(mockedList, atMost(2)).add("test"); // 至多调用 2 次
2. 参数匹配
(1)精确匹配参数
verify(mockObject).methodName(expectedArgument);
- 作用:验证
methodName
方法被调用时,传递的参数与expectedArgument
完全匹配。 - 示例:
List<String> mockedList = mock(List.class); mockedList.add("test"); verify(mockedList).add("test"); // 验证 add 方法被调用,且参数为 "test"
(2)使用匹配器(Matchers)
Mockito 提供了多种参数匹配器,用于更灵活的验证:
any()
:匹配任意值(包括null
)。eq()
:匹配特定值。anyInt()
、anyString()
等:匹配特定类型的任意值。isNull()
、isNotNull()
:匹配null
或非null
值。
示例:
List<String> mockedList = mock(List.class);
mockedList.add("test");
verify(mockedList).add(anyString()); // 验证 add 方法被调用,且参数为任意字符串
verify(mockedList).add(eq("test")); // 验证 add 方法被调用,且参数为 "test"
注意:
- 如果使用参数匹配器(如
any()
),所有参数都必须使用匹配器,不能混合使用精确值和匹配器。// 错误示例 verify(mockedList).add("test", anyInt()); // 编译错误 // 正确示例 verify(mockedList).add(eq("test"), anyInt()); // 正确
3. 验证调用顺序
Mockito 允许验证方法的调用顺序:
InOrder inOrder = inOrder(mockObject);
inOrder.verify(mockObject).firstMethod();
inOrder.verify(mockObject).secondMethod();
- 作用:验证
firstMethod
在secondMethod
之前被调用。 - 示例:
List<String> mockedList = mock(List.class); mockedList.add("first"); mockedList.add("second"); InOrder inOrder = inOrder(mockedList); inOrder.verify(mockedList).add("first"); inOrder.verify(mockedList).add("second");
4. 验证无更多交互
verifyNoMoreInteractions(mockObject);
- 作用:验证
mockObject
除了已经验证的调用外,没有其他交互。 - 示例:
List<String> mockedList = mock(List.class); mockedList.add("test"); verify(mockedList).add("test"); verifyNoMoreInteractions(mockedList); // 确保没有其他调用
5. 验证超时
verify(mockObject, timeout(millis)).methodName();
- 作用:验证
methodName
方法在指定时间内被调用。 - 示例:
List<String> mockedList = mock(List.class); new Thread(() -> { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } mockedList.add("test"); }).start(); verify(mockedList, timeout(200)).add("test"); // 验证在 200ms 内被调用
6. 总结
verify
是 Mockito 中用于验证方法调用的核心工具。通过它,你可以:
- 验证方法是否被调用。
- 验证方法调用的次数。
- 验证方法调用的参数。
- 验证方法调用的顺序。
- 验证无更多交互。
- 验证超时调用。