# Maui Unit Testing

> 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.

- Skill: `dotnet/maui-unit-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add dotnet/maui-unit-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dotnet/maui-unit-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: .NET (Microsoft) (https://skillmd.com/u/dotnet)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dotnet/maui-unit-testing

---


# 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

1. Identify what is being tested: ViewModel, service, navigation abstraction,
   storage wrapper, platform capability, or UI integration.
2. For ViewModels and services, create a normal xUnit test project.
3. Introduce interfaces around platform APIs such as geolocation, preferences,
   secure storage, media picker, file picker, and navigation.
4. Use fakes or mocks for those interfaces in unit tests.
5. Use device/integration tests only for behavior that requires handlers,
   platform permissions, native controls, or OS services.
6. 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.

```csharp
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.

```csharp
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.

