Android音频之单元测试

 

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)测试依赖问题

  • 确保测试模块依赖了正确的库(如 junitmockito)。
  • 对于仪器化测试,确保依赖了 androidx.test 相关库。

(3)测试性能问题

  • 避免在测试中执行耗时操作(如网络请求、数据库读写)。
  • 使用 mock 对象替代真实依赖。

8. 实践

要编译并测试 android/packages/services/Car/tests/carservice_unit_test/src/com/android/car/audio/CarAudioFocusUnitTest.java,你需要按照以下步骤操作。这个过程涉及 Android 源码的构建系统和测试框架。


1. 环境准备

在开始之前,确保你已经完成以下准备工作:

  • 下载并配置 Android 源码:确保你已经下载了完整的 Android 源码,并配置好了构建环境(如 repolunch 等)。
  • 安装依赖:确保你的系统已经安装了构建 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方法介绍

verifyMockito 框架中的一个核心方法,用于验证某个对象的特定方法是否被调用,以及调用的次数和参数是否符合预期。它是单元测试中非常重要的工具,尤其是在测试依赖其他对象的方法时。

以下是对 verify 用法的详细介绍:


1. 基本用法

(1)验证方法是否被调用

verify(mockObject).methodName();
  • 作用:验证 mockObjectmethodName 方法是否被调用过一次。
  • 示例
    List<String> mockedList = mock(List.class);
    mockedList.add("test");
    verify(mockedList).add("test");
    

(2)验证方法被调用的次数

verify(mockObject, times(n)).methodName();
  • 作用:验证 mockObjectmethodName 方法被调用了 n 次。
  • 示例
    List<String> mockedList = mock(List.class);
    mockedList.add("test");
    mockedList.add("test");
    verify(mockedList, times(2)).add("test");
    

(3)验证方法从未被调用

verify(mockObject, never()).methodName();
  • 作用:验证 mockObjectmethodName 方法从未被调用。
  • 示例
    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();
  • 作用:验证 firstMethodsecondMethod 之前被调用。
  • 示例
    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 中用于验证方法调用的核心工具。通过它,你可以:

  • 验证方法是否被调用。
  • 验证方法调用的次数。
  • 验证方法调用的参数。
  • 验证方法调用的顺序。
  • 验证无更多交互。
  • 验证超时调用。