The Health Connect Testing library (androidx.health.connect:connect-testing
)
simplifies the creation of automated tests. You can use this library to verify
the behavior of your application and validate that it responds correctly to
uncommon cases, which are hard to test manually.
You can use the library to create local unit tests, which typically verify the behavior of the classes in your app that interact with the Health Connect client.
To start using the library, add it as a test dependency:
testImplementation("androidx.health.connect:connect-testing:1.0.0-alpha01")
The entry point to the library is the FakeHealthConnectClient
class, which you
use in tests to replace the HealthConnectClient
. The FakeHealthConnectClient
has the following features:
- An in-memory representation of records, so you can insert, remove, delete and read them
- Generation of change tokens and change tracking
- Pagination for records and changes
- Aggregation responses are supported with stubs
- Allows any function to throw exceptions
- A
FakePermissionController
that can be used to emulate permissions checks
To learn more about replacing dependencies in tests, read Dependency Injection in Android. To know more about fakes, read Using test doubles in Android.
For example, if the class that interacts with the client is called
HealthConnectManager
and it takes a HealthConnectClient
as a dependency, it
would look like:
class HealthConnectManager(
private val healthConnectClient: HealthConnectClient,
...
) { }
In tests, you can pass a fake to your class under test instead:
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)
}
}
This test verifies that the fictional fetchReport
function in
HealthConnectManager
properly filters records by activity.
Verifying exceptions
Almost every call to HealthConnectClient
can throw exceptions. For example,
the documentation for insertRecords
mentions these exceptions:
@throws android.os.RemoteException
for any IPC transportation failures.@throws SecurityException
for requests with unpermitted access.@throws java.io.IOException
for any disk I/O issues.
These exceptions cover cases like a bad connection or no space left on the device. Your app must react correctly to these runtime issues, as they can happen at any time.
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)
}
Aggregation
Aggregation calls don't have fake implementations. Instead, aggregation calls
use stubs that you can program to behave in a certain way. You can access the
stubs through the overrides
property of the FakeHealthConnectClient
.
For example, you can program the aggregate function to return a specific result:
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)
Then, you can verify that your class under test, HealthConnectManager
in this
case, processed the result correctly:
// 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)
Permissions
The testing library includes a FakePermissionController
, which can be passed
as a dependency to FakeHealthConnectClient
.
Your subject under tests can use the PermissionController—through
the
permissionController
property of the HealthConnectClient
interface—to check
for permissions. This is typically done before every call to the client.
To test this functionality, you can set which permissions are available using
the 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)
}
Pagination
Pagination is a very common source of bugs, so FakeHealthConnectClient
provides mechanisms to help you verify that your paging implementation for
records and changes behaves correctly.
Your subject under test, HealthConnectManager
in our example, can specify the
page size in the ReadRecordsRequest
:
fun fetchRecordsReport(pageSize: Int = 1000) }
val pagedRequest =
ReadRecordsRequest(
timeRangeFilter = ...,
recordType = ...,
pageToken = page1.pageToken,
pageSize = pageSize,
)
val page = client.readRecords(pagedRequest)
...
Setting the page size to a small value, such as 2, lets you easily test
pagination. For example, you can insert 5 records so that readRecords
returns 3
different pages:
@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)
}
Test data
The library doesn't include APIs to generate fake data yet, but you can use the data and generators used by the library in Android Code Search.
Stubs
The overrides
property of FakeHealthConnectClient
lets you program (or
stub out) any of its functions so that they throw exceptions when called.
Aggregation calls can also return arbitrary data, and it supports enqueuing
multiple responses. See Stub
and MutableStub
for more information.
Summary of edge cases
- Verify that your app behaves as expected when the client throws exceptions. Check the documentation of each function to figure out what exceptions you should check.
- Verify that every call you make to the client is preceded by the proper permissions check.
- Verify your pagination implementation.
- Verify what happens when you fetch multiple pages but one has an expired token.