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 操作流程:
- @Before 阶段:通过
CameraManager.openCamera()申请 CameraDevice 资源,并创建CameraCharacteristics对象用于后续验证。 - @Test 阶段:创建
CaptureRequest.Builder,配置测试所需的参数(如 AE 模式、3A 锁定),提交CaptureRequest到 HAL,并等待CaptureResult。 - @After 阶段:关闭
CameraCaptureSession,关闭CameraDevice,释放ImageReader或Surface资源。
避坑指南:不要将 CameraDevice 声明为静态变量。不同测试类之间的静态资源竞争是 CTS 测试中常见的失败原因。每个测试实例应拥有独立的 CameraDevice 引用。
4.3 生命周期与 HAL 行为验证的映射
HAL 层的行为验证与测试用例的生命周期紧密耦合。以下 SVG 图展示了从测试框架到 HAL 层的完整调用链,以及资源在生命周期各阶段的流转状态。
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 行为验证方法。