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
:指定要运行的测试类。-e class#func
: 指定要运行的测试类中具体函数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 中用于验证方法调用的核心工具。通过它,你可以:
- 验证方法是否被调用。
- 验证方法调用的次数。
- 验证方法调用的参数。
- 验证方法调用的顺序。
- 验证无更多交互。
- 验证超时调用。
在Android开发中使用JUnit4进行单元测试是保障代码质量的重要手段。以下是基于最新实践的详细指南:
一、环境配置与依赖
- 添加依赖
在项目的build.gradle
文件中添加JUnit4及Android测试库的依赖:dependencies { testImplementation 'junit:junit:4.13.2' // JUnit4核心库 androidTestImplementation 'androidx.test.ext:junit:1.1.3' // AndroidJUnit4适配 testImplementation 'org.mockito:mockito-core:3.11.2' // 模拟外部依赖(如网络请求) }
- AndroidManifest配置
若涉及Android上下文(如Context、资源等),需在AndroidManifest.xml
中添加测试运行器声明:<uses-library android:name="android.test.runner" /> <application android:targetPackage="com.example.app"> <!-- 替换为实际包名 --> ... </application>
二、测试类编写
- 基本结构
测试类通常位于src/test/java
目录,无需继承TestCase
类,直接使用JUnit4注解:
import org.junit.Test;
import static org.junit.Assert.*;
public class CalculatorTest {
@Test
public void testAdd() {
Calculator calculator = new Calculator();
int sum = calculator.add(2, 3);
assertEquals(5, sum); // 断言验证结果
}
@Test
public void testString() {
String str = null;
// 断言null或为空字符串
assertThat(str).isNullOrEmpty();
// 断言空字符串
assertThat("").isEmpty();
// 断言字符串相等 断言忽略大小写判断字符串相等
assertThat("Frodo").isEqualTo("Frodo").isEqualToIgnoringCase("frodo");
// 断言开始字符串 结束字符穿 字符串长度
assertThat("Frodo").startsWith("Fro").endsWith("do").hasSize(5);
// 断言包含字符串 不包含字符串
assertThat("Frodo").contains("rod").doesNotContain("fro");
// 断言字符串只出现过一次
assertThat("Frodo").containsOnlyOnce("do");
// 判断正则匹配
assertThat("Frodo").matches("..o.o").doesNotMatch(".*d");
}
@Test
public void testNumber() {
Integer num = null;
// 断言空
assertThat(num).isNull();
// 断言相等
assertThat(42).isEqualTo(42);
// 断言大于 大于等于
assertThat(42).isGreaterThan(38).isGreaterThanOrEqualTo(38);
// 断言小于 小于等于
assertThat(42).isLessThan(58).isLessThanOrEqualTo(58);
// 断言0
assertThat(0).isZero();
// 断言正数 非负数
assertThat(1).isPositive().isNotNegative();
// 断言负数 非正数
assertThat(-1).isNegative().isNotPositive();
}
}
- Android环境测试
若需访问Android组件(如Context),使用AndroidJUnit4
运行器:@RunWith(AndroidJUnit4.class) public class ContextTest { @Test public void useAppContext() { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("com.example.app", context.getPackageName()); } }
三、常用注解与生命周期
| 注解 | 作用描述 | 示例场景 |
|—————|————————————————————————–|——————————|
| @Test
| 标记测试方法,支持timeout
(超时检测)和expected
(预期异常)参数 | @Test(timeout=1000)
|
| @Before
| 每个测试方法执行前运行,用于初始化资源 | 创建被测对象实例 |
| @After
| 每个测试方法执行后运行,用于释放资源 | 关闭数据库连接 |
| @BeforeClass
| 所有测试方法执行前运行一次(需静态方法) | 初始化全局配置 |
| @Ignore
| 忽略当前测试方法或类 | 临时跳过未完成的测试 |
四、高级技巧
- 模拟外部依赖(Mockito)
使用Mockito模拟接口或复杂对象,隔离被测代码:@Test public void testNetworkRequest() { ApiService mockService = Mockito.mock(ApiService.class); when(mockService.getData()).thenReturn("Mocked Response"); // 定义模拟行为 String result = new DataProcessor(mockService).fetchData(); assertEquals("Mocked Response", result); }
-
参数化测试
通过@ParameterizedTest
实现多组数据驱动测试(需JUnit5或扩展库),减少重复代码。 - 测试覆盖率优化
结合Android Studio的覆盖率工具(Run with Coverage
),确保覆盖边界条件和异常分支。
五、运行与调试
• 运行单个测试:右键点击测试类/方法,选择Run 'testName'
。
• 命令行执行:./gradlew test
(运行所有单元测试)或./gradlew connectedAndroidTest
(设备测试)。
• 查看结果:在Android Studio的Run窗口查看通过/失败的测试列表,点击失败项可定位问题代码。
注意事项
• 隔离性:每个测试方法需独立,避免共享状态导致干扰。 • 性能优化:避免在单元测试中执行耗时操作(如I/O),必要时使用Mock或内存数据库。 • 持续集成:将测试流程集成到CI/CD中,确保每次提交自动验证。