Testing Plan: Maui.Health Unit + Device Integration Tests
Context
Maui.Health has zero tests. We need to add both unit tests for pure logic (runs in CI without devices) and device integration tests that verify the actual Health Connect / HealthKit calls work on real Android emulators and iOS simulators.
Project conventions
- Framework: xUnit (recommended for MAUI device test compatibility)
- Assertions: Shouldly
- Location:
tests/ directory
Part 1: Unit Tests (no device needed)
Project
tests/Maui.Health.Tests/Maui.Health.Tests.csproj
- Target:
net9.0 (plain, no platform TFMs)
- NuGet:
xunit, xunit.runner.visualstudio, Shouldly, Microsoft.NET.Test.Sdk
What to test
Mapping logic (pure functions):
MetricDtoExtensions.GetHealthDataType<T>() - all DTO types map correctly
MetricDtoExtensions.GetRequiredPermission<T>() - returns correct permission type
GetAggregateMetricInfo() (Android) - maps HealthDataType to record class / metric field / unit
GetStatisticsInfo() (iOS) - maps HealthDataType to HKStatisticsOptions / HKUnit
GetUnitString() (iOS) - returns correct unit string per type
IsCumulativeType() - Steps and ActiveCalories are cumulative, others are not
Token serialization (iOS differential sync):
GetChangesToken produces valid JSON with Anchor + DataTypes
- JSON token round-trips correctly (serialize then deserialize)
- Anchor value is preserved through serialization
Workout duplicate detection:
FindDuplicates() - same activity type, different sources, overlapping times → detected
FindDuplicates() - same source → NOT detected (not a cross-source duplicate)
FindDuplicates() - different activity types → NOT detected
FindDuplicates() - times outside threshold → NOT detected
FindDuplicates() - activity type filter works
Model validation:
AggregatedResult has no Id/DataOrigin (structural assertion)
HealthChangesResult round-trips HasMore, NextToken, Changes list
HealthChange correctly holds Type, RecordId, DataType
Note: Some of the mapping functions are private static in platform files. We have two options:
- Extract them to
internal static helpers and use [InternalsVisibleTo]
- Test them indirectly through the public API
Recommend option 1 with [InternalsVisibleTo("Maui.Health.Tests")] in the main project.
Part 2: Device Integration Tests
Project
tests/Maui.Health.DeviceTests/Maui.Health.DeviceTests.csproj
- This is a full .NET MAUI app that deploys to device/emulator
- Target frameworks:
net10.0-android, net10.0-ios
- Uses xUnit with xharness for headless execution
- NuGet:
xunit, Microsoft.DotNet.XHarness.TestRunners.Common, Microsoft.DotNet.XHarness.DefaultAndroidEntryPoint.Xunit
- References
Maui.Health project directly
Android setup requirements
- Emulator with API 34+ (Health Connect built-in)
- Tests auto-grant permissions via
adb shell pm grant or test helper
- Tests write data first, then verify reads/aggregates/deletes
iOS setup requirements
- Simulator with iOS 17+
- HealthKit available on simulator
- Tests write data first via
WriteHealthData, then verify reads
- Note: iOS doesn't allow checking read permission status (by design), so we just verify data comes back
Test scenarios
Read/Write round-trip:
- Write a StepsDto → Read it back → verify Count matches
- Write an ActiveCaloriesBurnedDto → Read it back → verify Energy matches
- Write a WeightDto → Read it back → verify Value matches
Delete:
- Write a StepsDto → Get its ID from read → Delete by ID → Read again → verify gone
Aggregate:
- Write multiple StepsDto records → GetAggregatedHealthData → verify sum
- Write calories → GetAggregatedHealthData → verify sum
Aggregate by interval:
- Write steps spanning 3 days → GetAggregatedHealthDataByInterval with 1-day interval → verify 3 buckets
Differential sync:
- GetChangesToken → Write new data → GetChanges → verify upsert changes returned
- GetChanges again with new token → verify 0 changes (nothing new)
GetHealthRecord (read single):
- Write a record → Read all to get ID → GetHealthRecord by ID → verify matches
Permissions:
- Request read permission → verify IsSuccess
Running device tests
# Android (requires emulator running)
dotnet build tests/Maui.Health.DeviceTests -f net10.0-android
# Deploy and run via xharness
xharness android test --apk=path/to/app.apk --output-directory=out
# iOS (requires simulator, macOS only)
dotnet build tests/Maui.Health.DeviceTests -f net10.0-ios
# Deploy and run via xharness
xharness apple test --app=path/to/app.app --output-directory=out --target=ios-simulator-64
Files to create
Unit tests
tests/Maui.Health.Tests/Maui.Health.Tests.csproj
tests/Maui.Health.Tests/MetricDtoExtensionsTests.cs
tests/Maui.Health.Tests/DuplicateDetectionTests.cs
tests/Maui.Health.Tests/AggregateMetricMappingTests.cs
tests/Maui.Health.Tests/TokenSerializationTests.cs
tests/Maui.Health.Tests/ModelTests.cs
Device tests
tests/Maui.Health.DeviceTests/Maui.Health.DeviceTests.csproj
tests/Maui.Health.DeviceTests/MauiProgram.cs
tests/Maui.Health.DeviceTests/MainPage.xaml + .cs (test runner UI)
tests/Maui.Health.DeviceTests/Tests/ReadWriteTests.cs
tests/Maui.Health.DeviceTests/Tests/DeleteTests.cs
tests/Maui.Health.DeviceTests/Tests/AggregateTests.cs
tests/Maui.Health.DeviceTests/Tests/DifferentialSyncTests.cs
Modifications
src/Maui.Health/Maui.Health.csproj - Add [InternalsVisibleTo("Maui.Health.Tests")]
- Solution file - Add both test projects
Verification
dotnet test tests/Maui.Health.Tests/ runs unit tests in CI
- Device tests deploy and run on Android emulator / iOS simulator via xharness
Testing Plan: Maui.Health Unit + Device Integration Tests
Context
Maui.Health has zero tests. We need to add both unit tests for pure logic (runs in CI without devices) and device integration tests that verify the actual Health Connect / HealthKit calls work on real Android emulators and iOS simulators.
Project conventions
tests/directoryPart 1: Unit Tests (no device needed)
Project
tests/Maui.Health.Tests/Maui.Health.Tests.csprojnet9.0(plain, no platform TFMs)xunit,xunit.runner.visualstudio,Shouldly,Microsoft.NET.Test.SdkWhat to test
Mapping logic (pure functions):
MetricDtoExtensions.GetHealthDataType<T>()- all DTO types map correctlyMetricDtoExtensions.GetRequiredPermission<T>()- returns correct permission typeGetAggregateMetricInfo()(Android) - maps HealthDataType to record class / metric field / unitGetStatisticsInfo()(iOS) - maps HealthDataType to HKStatisticsOptions / HKUnitGetUnitString()(iOS) - returns correct unit string per typeIsCumulativeType()- Steps and ActiveCalories are cumulative, others are notToken serialization (iOS differential sync):
GetChangesTokenproduces valid JSON with Anchor + DataTypesWorkout duplicate detection:
FindDuplicates()- same activity type, different sources, overlapping times → detectedFindDuplicates()- same source → NOT detected (not a cross-source duplicate)FindDuplicates()- different activity types → NOT detectedFindDuplicates()- times outside threshold → NOT detectedFindDuplicates()- activity type filter worksModel validation:
AggregatedResulthas no Id/DataOrigin (structural assertion)HealthChangesResultround-trips HasMore, NextToken, Changes listHealthChangecorrectly holds Type, RecordId, DataTypeNote: Some of the mapping functions are
private staticin platform files. We have two options:internal statichelpers and use[InternalsVisibleTo]Recommend option 1 with
[InternalsVisibleTo("Maui.Health.Tests")]in the main project.Part 2: Device Integration Tests
Project
tests/Maui.Health.DeviceTests/Maui.Health.DeviceTests.csprojnet10.0-android,net10.0-iosxunit,Microsoft.DotNet.XHarness.TestRunners.Common,Microsoft.DotNet.XHarness.DefaultAndroidEntryPoint.XunitMaui.Healthproject directlyAndroid setup requirements
adb shell pm grantor test helperiOS setup requirements
WriteHealthData, then verify readsTest scenarios
Read/Write round-trip:
Delete:
Aggregate:
Aggregate by interval:
Differential sync:
GetHealthRecord (read single):
Permissions:
Running device tests
Files to create
Unit tests
tests/Maui.Health.Tests/Maui.Health.Tests.csprojtests/Maui.Health.Tests/MetricDtoExtensionsTests.cstests/Maui.Health.Tests/DuplicateDetectionTests.cstests/Maui.Health.Tests/AggregateMetricMappingTests.cstests/Maui.Health.Tests/TokenSerializationTests.cstests/Maui.Health.Tests/ModelTests.csDevice tests
tests/Maui.Health.DeviceTests/Maui.Health.DeviceTests.csprojtests/Maui.Health.DeviceTests/MauiProgram.cstests/Maui.Health.DeviceTests/MainPage.xaml+.cs(test runner UI)tests/Maui.Health.DeviceTests/Tests/ReadWriteTests.cstests/Maui.Health.DeviceTests/Tests/DeleteTests.cstests/Maui.Health.DeviceTests/Tests/AggregateTests.cstests/Maui.Health.DeviceTests/Tests/DifferentialSyncTests.csModifications
src/Maui.Health/Maui.Health.csproj- Add[InternalsVisibleTo("Maui.Health.Tests")]Verification
dotnet test tests/Maui.Health.Tests/runs unit tests in CI