4. 测试用例生命周期:@Before、@Test、@After、测试资源申请与释放

在 CTS Camera 测试框架中,每个测试用例的执行都遵循严格的生命周期管理。理解 @Before、@Test、@After 注解的作用域以及测试资源的申请与释放机制,是编写稳定、可复现测试用例的基础。错误的资源管理会导致测试间相互污染,产生假阳性或假阴性结果。

4.1 生命周期注解执行顺序

JUnit 框架为每个测试方法提供了三个关键的生命周期钩子。在 Camera CTS 测试中,这些钩子被用于管理 CameraManager、CameraDevice、CaptureSession 等重量级资源。

注解 执行时机 典型用途(Camera CTS) 执行次数
@Before 每个 @Test 方法执行之前 打开 CameraManager、获取 CameraCharacteristics、创建 CameraDevice 每个测试方法 1 次
@Test 核心测试逻辑 提交 CaptureRequest、验证 HAL 返回的 Result、检查 metadata 1 次
@After 每个 @Test 方法执行之后(无论成功或失败) 关闭 CaptureSession、关闭 CameraDevice、释放 ImageReader 每个测试方法 1 次
关键原则:@After 中的资源释放代码必须放在 finally 块或使用 try-finally 结构,确保即使 @Test 抛出异常,资源也能被正确回收。Camera HAL 层对未释放的 CameraDevice 非常敏感,可能导致后续测试无法打开相机。

4.2 测试资源申请与释放的典型流程

一个标准的 Camera CTS 测试用例,其资源生命周期通常遵循“申请 → 使用 → 验证 → 释放”的闭环。下面是一个典型的 CameraDevice 操作流程:

  1. @Before 阶段:通过 CameraManager.openCamera() 申请 CameraDevice 资源,并创建 CameraCharacteristics 对象用于后续验证。
  2. @Test 阶段:创建 CaptureRequest.Builder,配置测试所需的参数(如 AE 模式、3A 锁定),提交 CaptureRequest 到 HAL,并等待 CaptureResult。
  3. @After 阶段:关闭 CameraCaptureSession,关闭 CameraDevice,释放 ImageReader 或 Surface 资源。
避坑指南:不要将 CameraDevice 声明为静态变量。不同测试类之间的静态资源竞争是 CTS 测试中常见的失败原因。每个测试实例应拥有独立的 CameraDevice 引用。

4.3 生命周期与 HAL 行为验证的映射

HAL 层的行为验证与测试用例的生命周期紧密耦合。以下 SVG 图展示了从测试框架到 HAL 层的完整调用链,以及资源在生命周期各阶段的流转状态。

测试用例生命周期与 HAL 资源流转图 测试用例生命周期与 HAL 资源流转 @Before 阶段 资源申请 CameraManager.openCamera() 创建 CameraCharacteristics @Test 阶段 HAL 行为验证 CaptureRequest.Builder 配置 capture() → 等待 CaptureResult @After 阶段 资源释放 close CaptureSession close CameraDevice Camera HAL 层 open() → configureStreams() → processCaptureRequest() → flush() → close() 说明: 1. @Before 申请 CameraDevice 时,HAL 的 open() 被调用。 2. @Test 提交 CaptureRequest 时,HAL 的 processCaptureRequest() 被调用。 3. @After 关闭 CameraDevice 时,HAL 的 close() 被调用。 4. 任何阶段的异常都应在 @After 中确保 close() 执行。

4.4 代码模板:完整的生命周期实现

以下是一个符合 CTS 规范的 Camera 测试用例骨架,展示了资源申请与释放的标准写法:

public class CameraLifecycleTest {
    private CameraManager mCameraManager;
    private CameraDevice mCameraDevice;
    private CameraCaptureSession mCaptureSession;
    private String mCameraId;

    @Before
    public void setUp() throws Exception {
        // 1. 获取 CameraManager
        mCameraManager = (CameraManager) 
            InstrumentationRegistry.getInstrumentation()
                .getContext().getSystemService(Context.CAMERA_SERVICE);
        
        // 2. 选择测试用的 CameraId(通常为 "0")
        mCameraId = "0";
        
        // 3. 打开 CameraDevice(同步等待)
        final CountDownLatch latch = new CountDownLatch(1);
        mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
            @Override
            public void onOpened(CameraDevice camera) {
                mCameraDevice = camera;
                latch.countDown();
            }
            @Override
            public void onDisconnected(CameraDevice camera) {
                latch.countDown();
            }
            @Override
            public void onError(CameraDevice camera, int error) {
                latch.countDown();
            }
        }, null);
        
        if (!latch.await(5, TimeUnit.SECONDS)) {
            throw new TimeoutException("Camera open timeout");
        }
        
        // 4. 验证 CameraDevice 非空
        assertNotNull("CameraDevice should not be null", mCameraDevice);
    }

    @Test
    public void testBasicCapture() throws Exception {
        // 1. 创建 CaptureRequest.Builder
        CaptureRequest.Builder requestBuilder = 
            mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        
        // 2. 配置测试参数(例如:强制 AE 锁定)
        requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true);
        
        // 3. 创建输出 Surface(使用 ImageReader)
        ImageReader imageReader = ImageReader.newInstance(640, 480, 
            ImageFormat.YUV_420_888, 2);
        requestBuilder.addTarget(imageReader.getSurface());
        
        // 4. 创建 CaptureSession 并提交请求
        final CountDownLatch resultLatch = new CountDownLatch(1);
        mCameraDevice.createCaptureSession(
            Arrays.asList(imageReader.getSurface()),
            new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(CameraCaptureSession session) {
                    mCaptureSession = session;
                    try {
                        session.capture(requestBuilder.build(), 
                            new CameraCaptureSession.CaptureCallback() {
                                @Override
                                public void onCaptureCompleted(
                                        CameraCaptureSession session,
                                        CaptureRequest request,
                                        TotalCaptureResult result) {
                                    // 5. 验证 HAL 返回的 Result
                                    Integer aeLockState = 
                                        result.get(CaptureResult.CONTROL_AE_LOCK);
                                    assertNotNull("AE lock state should not be null", 
                                        aeLockState);
                                    assertEquals("AE lock should be ON", 
                                        (Integer) 1, aeLockState);
                                    resultLatch.countDown();
                                }
                            }, null);
                    } catch (CameraAccessException e) {
                        fail("Capture failed: " + e.getMessage());
                    }
                }
                @Override
                public void onConfigureFailed(CameraCaptureSession session) {
                    fail("Session configuration failed");
                }
            }, null);
        
        if (!resultLatch.await(3, TimeUnit.SECONDS)) {
            fail("Capture result timeout");
        }
        
        // 6. 清理本次测试创建的临时资源
        imageReader.close();
    }

    @After
    public void tearDown() throws Exception {
        // 1. 关闭 CaptureSession
        if (mCaptureSession != null) {
            mCaptureSession.close();
            mCaptureSession = null;
        }
        
        // 2. 关闭 CameraDevice(关键:必须执行)
        if (mCameraDevice != null) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
        
        // 3. 注意:@After 中不要遗留任何 Camera 相关引用
    }
}
常见陷阱:
  • 资源泄漏:如果在 @Test 中创建了额外的 ImageReader 或 Surface,必须在 @Test 末尾或 @After 中释放。@After 只负责释放成员变量级别的资源。
  • 超时处理:openCamera() 和 capture() 都是异步操作,必须使用 CountDownLatch 同步等待。超时时间建议设置为 3-5 秒,避免测试挂死。
  • 异常传播:@Before 和 @After 方法声明抛出 Exception,确保任何底层 CameraAccessException 都能被 JUnit 捕获并报告为测试错误。

4.5 资源申请与释放的检查清单

在编写或审查 CTS Camera 测试用例时,可以使用以下清单确保生命周期管理的完整性:

检查项 描述 验证方式
CameraDevice 关闭 每个 openCamera() 必须有对应的 close() 在 @After 中调用 mCameraDevice.close()
CaptureSession 关闭 每个 createCaptureSession() 成功后必须有 close() 在 @After 中调用 mCaptureSession.close()
Surface/ImageReader 释放 测试中创建的临时 Surface 必须释放 在 @Test 末尾或 @After 中调用 close()
HandlerThread 清理 如果使用了 HandlerThread,必须 quitSafely() 在 @After 中调用 thread.quitSafely()
静态资源隔离 避免使用 static 变量持有 CameraDevice 代码审查,确保每个测试类有独立实例
异常安全 @After 中的释放代码不因异常而跳过 使用 try-finally 或 try-with-resources

4.6 生命周期中的 HAL 行为验证点

测试用例的生命周期不仅是资源管理,更是 HAL 行为验证的时机。以下表格总结了在生命周期的不同阶段,可以验证的 HAL 行为:

生命周期阶段 HAL 接口调用 可验证的 HAL 行为
@Before (openCamera) hal_open() 验证 HAL 返回的 CameraCharacteristics 中静态 metadata 的完整性(如 ANDROID_INFO_SUPPORTED_HARDWARE_LEVEL)
@Test (capture) hal_processCaptureRequest() 验证 HAL 返回的 CaptureResult 中动态 metadata 是否符合预期(如 AE 状态、AF 状态)
@Test (capture) hal_configureStreams() 验证 HAL 是否支持请求的 stream configuration(如输出尺寸、格式组合)
@After (close) hal_close() 验证 HAL 在 close() 后是否释放所有资源,不产生内存泄漏(通过 dumpsys 或 logcat 验证)
进阶技巧:在 @After 中关闭 CameraDevice 后,可以立即尝试再次打开同一个 CameraId。如果 HAL 实现正确,第二次 openCamera() 应该成功。如果失败,说明 HAL 的 close() 实现存在资源未完全释放的问题。这是一个简单但有效的 HAL 行为验证方法。