使用 Health Connect Testing 库创建单元测试

Health Connect 测试库 (androidx.health.connect:connect-testing) 可简化自动化测试的创建。您可以使用此库来验证应用的行为,并验证应用是否能正确响应难以手动测试的异常情况。

您可以使用该库创建本地单元测试,这些测试通常用于验证应用中与健康数据共享客户端交互的类的行为。

如需开始使用该库,请将其添加为测试依赖项:

 testImplementation("androidx.health.connect:connect-testing:1.0.0-alpha01")

该库的入口点是 FakeHealthConnectClient 类,您可以在测试中使用该类来替换 HealthConnectClientFakeHealthConnectClient 具有以下功能:

  • 记录的内存表示形式,因此您可以插入、移除、删除和读取记录
  • 变更令牌的生成和变更跟踪
  • 记录和变更的分页
  • 支持使用桩的聚合响应
  • 允许任何函数抛出异常
  • 可用于模拟权限检查的 FakePermissionController

如需详细了解如何在测试中替换依赖项,请参阅 Android 中的依赖项注入。如需详细了解伪对象,请参阅在 Android 中使用测试替身

例如,如果与客户端互动的类名为 HealthConnectManager,并且它将 HealthConnectClient 作为依赖项,则它看起来会像这样:

class HealthConnectManager(
    private val healthConnectClient: HealthConnectClient,
    ...
) { }

在测试中,您可以向被测类传递一个伪对象:

import androidx.health.connect.client.testing.ExperimentalTestingApi
import androidx.health.connect.client.testing.FakeHealthConnectClient
import kotlinx.coroutines.test.runTest

@OptIn(ExperimentalTestingApi::class)
class HealthConnectManagerTest {

    @Test
    fun readRecords_filterByActivity() = runTest {
        // Create a Fake with 2 running records.
        val fake = FakeHealthConnectClient()
        fake.insertRecords(listOf(fakeRunRecord1, fakeBikeRecord1))

        // Create a manager that depends on the fake.
        val manager = HealthConnectManager(fake)

        // Read running records only.
        val runningRecords = manager.fetchReport(activity = Running)

        // Verify that the records were filtered correctly.
        assertTrue(runningRecords.size == 1)
    }
}

此测试可验证 HealthConnectManager 中的虚构函数 fetchReport 是否能按活动正确过滤记录。

验证例外情况

几乎每次对 HealthConnectClient 的调用都可能会抛出异常。例如,insertRecords 的文档中提到了以下例外情况:

  • @throws android.os.RemoteException 中是否有任何 IPC 传输失败。
  • 对于未经许可的访问请求,值为 @throws SecurityException
  • @throws java.io.IOException,以解决任何磁盘 I/O 问题。

这些例外情况涵盖了连接状况不佳或设备上没有剩余空间等情况。您的应用必须对这些运行时问题做出正确反应,因为这些问题随时可能发生。

import androidx.health.connect.client.testing.stubs.stub

@Test
fun addRecords_throwsRemoteException_errorIsExposed() {
    // Create Fake that throws a RemoteException
    // when insertRecords is called.
    val fake = FakeHealthConnectClient()
    fake.overrides.insertRecords = stub { throw RemoteException() }

    // Create a manager that depends on the fake.
    val manager = HealthConnectManager(fake)

    // Insert a record.
    manager.addRecords(fakeRunRecord1)

    // Verify that the manager is exposing an error.
    assertTrue(manager.errors.size == 1)
}

集合

聚合调用没有虚假实现。相反,聚合调用使用您可以编程以特定方式运行的桩。您可以通过 FakeHealthConnectClientoverrides 属性访问桩。

例如,您可以将汇总函数编程为返回特定结果:

import androidx.health.connect.client.testing.AggregationResult
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.ExerciseSessionRecord
import java.time.Duration

@Test
fun aggregate() {
    // Create a fake result.
    val result =
        AggregationResult(metrics =
            buildMap {
                put(HeartRateRecord.BPM_AVG, 74.0)
                put(
                    ExerciseSessionRecord.EXERCISE_DURATION_TOTAL,
                    Duration.ofMinutes(30)
                )
            }
        )

    // Create a fake that always returns the fake
    // result when aggregate() is called.
    val fake = FakeHealthConnectClient()
    fake.overrides.aggregate = stub(result)

然后,您可以验证被测类(在本例中为 HealthConnectManager)是否正确处理了结果:

// Create a manager that depends on the fake.
val manager = HealthConnectManager(fake)
// Call the function that in turn calls aggregate on the client.
val report = manager.getHeartRateReport()

// Verify that the manager is exposing an error.
assertThat(report.bpmAverage).isEqualTo(74.0)

权限

测试库包含一个 FakePermissionController,可以作为依赖项传递给 FakeHealthConnectClient

受测对象可以使用 HealthConnectClient 接口的 PermissionController—through permissionController 属性来检查权限。此操作通常在每次调用客户端之前完成。

如需测试此功能,您可以使用 FakePermissionController 设置哪些权限可用:

import androidx.health.connect.client.testing.FakePermissionController

@Test
fun newRecords_noPermissions_errorIsExposed() {
    // Create a permission controller with no permissions.
    val permissionController = FakePermissionController(grantAll = false)

    // Create a fake client with the permission controller.
    val fake = FakeHealthConnectClient(permissionController = permissionController)

    // Create a manager that depends on the fake.
    val manager = HealthConnectManager(fake)

    // Call addRecords so that the permission check is made.
    manager.addRecords(fakeRunRecord1)

    // Verify that the manager is exposing an error.
    assertThat(manager.errors).hasSize(1)
}

分页

分页是 bug 的常见来源,因此 FakeHealthConnectClient 提供了多种机制来帮助您验证记录和更改的分页实现是否正常运行。

被测对象(在我们的示例中为 HealthConnectManager)可以在 ReadRecordsRequest 中指定页面大小:

fun fetchRecordsReport(pageSize: Int = 1000) }
    val pagedRequest =
        ReadRecordsRequest(
            timeRangeFilter = ...,
            recordType = ...,
            pageToken = page1.pageToken,
            pageSize = pageSize,
        )
    val page = client.readRecords(pagedRequest)
    ...

将页面大小设置为较小的值(例如 2)可让您测试分页。例如,您可以插入 5 条记录,以便 readRecords 返回 3 个不同的网页:

@Test
fun readRecords_multiplePages() = runTest {

    // Create a Fake with 2 running records.
    val fake = FakeHealthConnectClient()
    fake.insertRecords(generateRunningRecords(5))

    // Create a manager that depends on the fake.
    val manager = HealthConnectManager(fake)

    // Read records with a page size of 2.
    val report = manager.generateReport(pageSize = 2)

    // Verify that all the pages were processed correctly.
    assertTrue(report.records.size == 5)
}

测试数据

该库目前不包含用于生成虚假数据的 API,但您可以在 Android 代码搜索中使用该库所用的数据和生成器。

如需在测试中模拟元数据值,您可以使用 MetadataTestHelper。这提供了 populatedWithTestValues() 扩展函数,该函数可模拟健康数据共享在插入记录期间填充元数据值的过程。

存根

通过 FakeHealthConnectClientoverrides 属性,您可以对它的任何函数进行编程(或存根化),以便在调用这些函数时抛出异常。聚合调用还可以返回任意数据,并且支持将多个响应排入队列。如需了解详情,请参阅 StubMutableStub

边缘情况摘要

  • 验证当客户端抛出异常时,应用是否按预期运行。 请查看每个函数的文档,了解应检查哪些异常。
  • 验证您对客户端的每次调用之前是否都进行了适当的权限检查。
  • 验证您的分页实现。
  • 验证在提取多个网页但其中一个网页的令牌已过期时会发生什么情况。