MAUI Unit Testing
Use this skill to make MAUI app logic testable without requiring every test to
start a device or simulator. Keep UI framework dependencies at the edges.
Workflow
- Identify what is being tested: ViewModel, service, navigation abstraction,
storage wrapper, platform capability, or UI integration.
- For ViewModels and services, create a normal xUnit test project.
- Introduce interfaces around platform APIs such as geolocation, preferences,
secure storage, media picker, file picker, and navigation.
- Use fakes or mocks for those interfaces in unit tests.
- Use device/integration tests only for behavior that requires handlers,
platform permissions, native controls, or OS services.
- If a test or tooling project must reference a MAUI app project, use the
AppProjectReference support provided by
Microsoft.Maui.Build.AppProjectReference
instead of forcing the app project to behave like a normal class library.
Test Boundary Guide
| Test target |
Test type |
| ViewModel commands, validation, state transitions |
Unit test |
| API/data service with fake handler |
Unit test |
| Secure storage wrapper behavior with fake storage |
Unit test |
| Shell route strings and navigation abstraction calls |
Unit test |
| Handler rendering, native permissions, real pickers |
Device/integration test |
| End-to-end UI flow in a running app |
DevFlow/runtime automation |
ViewModel Test Pattern
Always show at least a happy-path test AND an error/edge-case test. Use a
mocking library (NSubstitute, Moq) or a hand-rolled fake — both are valid.
public sealed class ProductsViewModelTests
{
[Fact]
public async Task LoadAsync_ServiceReturnsProducts_PopulatesProducts()
{
var service = Substitute.For<IProductsService>();
service.LoadAsync().Returns([new Product("Coffee")]);
var viewModel = new ProductsViewModel(service);
await viewModel.LoadAsync();
Assert.Single(viewModel.Products);
Assert.Equal("Coffee", viewModel.Products[0].Name);
}
[Fact]
public async Task LoadAsync_ServiceThrows_SetsErrorState()
{
var service = Substitute.For<IProductsService>();
service.LoadAsync().Returns(Task.FromException<IReadOnlyList<Product>>(
new HttpRequestException("offline")));
var viewModel = new ProductsViewModel(service);
await viewModel.LoadAsync();
Assert.Empty(viewModel.Products);
Assert.NotNull(viewModel.ErrorMessage);
}
}
Platform Abstraction Pattern
MAUI exposes injectable interfaces for many platform services. Prefer the built-in
interface when it exists, such as IGeolocation, IFileSystem, IPreferences,
IConnectivity, or ISecureStorage. Create a custom wrapper when the app needs
to combine platform APIs, normalize errors, or expose an app-specific contract.
public interface ILocationService
{
Task<Location?> GetLocationAsync(CancellationToken cancellationToken);
}
ViewModels depend on ILocationService, not Geolocation.Default directly.
The production MAUI project registers the platform implementation; tests pass a
fake implementation.
Anti-Patterns
- Do not call
MauiProgram.CreateMauiApp() in ordinary ViewModel unit tests.
- Do not require a simulator for tests that only validate C# state transitions.
- Do not use static MAUI platform services directly from ViewModels if the logic
needs deterministic unit tests.
- Do not call
MainThread.BeginInvokeOnMainThread or Dispatcher.Dispatch
inside synchronous ViewModel command logic; in plain xUnit hosts there may be
no initialized platform dispatcher. Keep dispatching at the UI layer or behind
an injectable dispatcher abstraction.
- Do not put UI automation expectations in unit tests; use DevFlow/runtime
automation for running-app behavior.
Validation Checklist
- ViewModel tests run without a device.
- Platform APIs are behind interfaces or wrappers.
- Test project references follow the repo's package management conventions.
- Device-only behavior is explicitly separated from unit-testable logic.
1---2name: maui-unit-testing3description: Add MAUI app tests around ViewModels, services, platform abstractions, and unit/integration/device boundaries. USE FOR: xUnit, avoiding `MauiProgram.CreateMauiApp` in unit tests, fakes/mocks, injectable Essentials interfaces, `PlatformNotSupportedException`, navigation/data services, AppProjectReference, device-test decisions. DO NOT USE FOR: UI automation or profiling.4---56# MAUI Unit Testing78Use this skill to make MAUI app logic testable without requiring every test to9start a device or simulator. Keep UI framework dependencies at the edges.1011## Workflow12131. Identify what is being tested: ViewModel, service, navigation abstraction,14 storage wrapper, platform capability, or UI integration.152. For ViewModels and services, create a normal xUnit test project.163. Introduce interfaces around platform APIs such as geolocation, preferences,17 secure storage, media picker, file picker, and navigation.184. Use fakes or mocks for those interfaces in unit tests.195. Use device/integration tests only for behavior that requires handlers,20 platform permissions, native controls, or OS services.216. If a test or tooling project must reference a MAUI app project, use the22 AppProjectReference support provided by `Microsoft.Maui.Build.AppProjectReference`23 instead of forcing the app project to behave like a normal class library.2425## Test Boundary Guide2627| Test target | Test type |28| --- | --- |29| ViewModel commands, validation, state transitions | Unit test |30| API/data service with fake handler | Unit test |31| Secure storage wrapper behavior with fake storage | Unit test |32| Shell route strings and navigation abstraction calls | Unit test |33| Handler rendering, native permissions, real pickers | Device/integration test |34| End-to-end UI flow in a running app | DevFlow/runtime automation |3536## ViewModel Test Pattern3738Always show at least a happy-path test AND an error/edge-case test. Use a39mocking library (NSubstitute, Moq) or a hand-rolled fake — both are valid.4041```csharp42public sealed class ProductsViewModelTests43{44 [Fact]45 public async Task LoadAsync_ServiceReturnsProducts_PopulatesProducts()46 {47 var service = Substitute.For<IProductsService>();48 service.LoadAsync().Returns([new Product("Coffee")]);49 var viewModel = new ProductsViewModel(service);5051 await viewModel.LoadAsync();5253 Assert.Single(viewModel.Products);54 Assert.Equal("Coffee", viewModel.Products[0].Name);55 }5657 [Fact]58 public async Task LoadAsync_ServiceThrows_SetsErrorState()59 {60 var service = Substitute.For<IProductsService>();61 service.LoadAsync().Returns(Task.FromException<IReadOnlyList<Product>>(62 new HttpRequestException("offline")));63 var viewModel = new ProductsViewModel(service);6465 await viewModel.LoadAsync();6667 Assert.Empty(viewModel.Products);68 Assert.NotNull(viewModel.ErrorMessage);69 }70}71```7273## Platform Abstraction Pattern7475MAUI exposes injectable interfaces for many platform services. Prefer the built-in76interface when it exists, such as `IGeolocation`, `IFileSystem`, `IPreferences`,77`IConnectivity`, or `ISecureStorage`. Create a custom wrapper when the app needs78to combine platform APIs, normalize errors, or expose an app-specific contract.7980```csharp81public interface ILocationService82{83 Task<Location?> GetLocationAsync(CancellationToken cancellationToken);84}85```8687ViewModels depend on `ILocationService`, not `Geolocation.Default` directly.88The production MAUI project registers the platform implementation; tests pass a89fake implementation.9091## Anti-Patterns9293- Do not call `MauiProgram.CreateMauiApp()` in ordinary ViewModel unit tests.94- Do not require a simulator for tests that only validate C# state transitions.95- Do not use static MAUI platform services directly from ViewModels if the logic96 needs deterministic unit tests.97- Do not call `MainThread.BeginInvokeOnMainThread` or `Dispatcher.Dispatch`98 inside synchronous ViewModel command logic; in plain xUnit hosts there may be99 no initialized platform dispatcher. Keep dispatching at the UI layer or behind100 an injectable dispatcher abstraction.101- Do not put UI automation expectations in unit tests; use DevFlow/runtime102 automation for running-app behavior.103104## Validation Checklist105106- ViewModel tests run without a device.107- Platform APIs are behind interfaces or wrappers.108- Test project references follow the repo's package management conventions.109- Device-only behavior is explicitly separated from unit-testable logic.