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:指定要运行的测试类。
  • -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方法介绍

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 中用于验证方法调用的核心工具。通过它,你可以:

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

在Android开发中使用JUnit4进行单元测试是保障代码质量的重要手段。以下是基于最新实践的详细指南:

一、环境配置与依赖

  1. 添加依赖
    在项目的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'  // 模拟外部依赖(如网络请求)
    }
    
  2. AndroidManifest配置
    若涉及Android上下文(如Context、资源等),需在AndroidManifest.xml中添加测试运行器声明:
    <uses-library android:name="android.test.runner" />
    <application android:targetPackage="com.example.app">  <!-- 替换为实际包名 -->
        ...
    </application>
    

二、测试类编写

  1. 基本结构

https://www.cnblogs.com/jpfss/p/10955009.html

测试类通常位于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();  
        }  
   }
  1. 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 | 忽略当前测试方法或类 | 临时跳过未完成的测试 |


四、高级技巧

  1. 模拟外部依赖(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);
    }
    
  2. 参数化测试
    通过@ParameterizedTest实现多组数据驱动测试(需JUnit5或扩展库),减少重复代码。

  3. 测试覆盖率优化
    结合Android Studio的覆盖率工具(Run with Coverage),确保覆盖边界条件和异常分支。

五、运行与调试

运行单个测试:右键点击测试类/方法,选择Run 'testName'。 • 命令行执行./gradlew test(运行所有单元测试)或./gradlew connectedAndroidTest(设备测试)。 • 查看结果:在Android Studio的Run窗口查看通过/失败的测试列表,点击失败项可定位问题代码。


注意事项

隔离性:每个测试方法需独立,避免共享状态导致干扰。 • 性能优化:避免在单元测试中执行耗时操作(如I/O),必要时使用Mock或内存数据库。 • 持续集成:将测试流程集成到CI/CD中,确保每次提交自动验证。