mcpField
M
maui-unit-testing

maui-unit-testing

github.com/davidortinauTesting & quality17665 installsUpdated 16w ago
Repository

About maui-unit-testing

A .NET MAUI testing skill covering unit-test setup, ViewModel patterns, and common pitfalls. It helps developers keep ViewModels testable by isolating MAUI platform APIs behind injectable interfaces.

Key features

  1. 01Guidance on test target frameworks and project output
  2. 02Injectable interfaces for MAUI APIs
  3. 03ViewModel state and command testing
  4. 04Mocking strategies for MAUI services
  5. 05Async-flow and CI testing tips

Use cases

  1. 01Set up unit tests for a MAUI app
  2. 02Test ViewModels without launching the UI
  3. 03Run regression checks with dotnet test

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/davidortinau/maui-skills --skill maui-unit-testing

After installing, type/maui-unit-testingin Claude Code to confirm it loaded.

Bundled filesreferences/unit-testing-api.md

Details

From the project README

.NET MAUI Unit Testing — Gotchas & Best Practices

For project templates, xUnit examples, ViewModel test patterns, and CLI commands, see references/unit-testing-api.md.

⚠️ TFM Trap: Don't Use Platform-Specific TFMs

<!-- ❌ xUnit can't run on platform-specific TFMs -->
<TargetFramework>net9.0-ios</TargetFramework>

<!-- ✅ Use plain .NET TFM for desktop test host -->
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>

⚠️ OutputType Trap: App Project as Test Dependency

If your test project references the app project, the app tries to build as Exe for the test TFM — this fails. Add a conditional OutputType:

<!-- ❌ App .csproj without conditional — breaks test builds -->
<OutputType>Exe</OutputType>

<!-- ✅ Library for test TFM, Exe for platform TFMs -->
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
  <OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'net9.0'">
  <OutputType>Exe</OutputType>
</PropertyGroup>

Common Mistakes

❌ Using Static MAUI APIs in ViewModels

ViewModels that directly call static MAUI APIs are untestable:

// ❌ Untestable — Shell.Current requires a running MAUI app
public async Task GoToDetail(int id)
    => await Shell.Current.GoToAsync($"detail?id={id}");

// ✅ Inject an interface — fully testable
public class MyViewModel(INavigationService nav)
{
    public async Task GoToDetail(int id)
        => await nav.GoToAsync($"detail?id={id}");
}

Static APIs to wrap behind interfaces:

  • Shell.Current → INavigationService
  • Application.Current → avoid entirely
  • SecureStorage.Default → ISecureStorage
  • Connectivity.Current → IConnectivity
❌ Asserting on UI Bindings Instead of ViewModel State
// ❌ Testing the binding — fragile, needs a running UI
Assert.Equal("Hello", label.Text);

// ✅ Testing the ViewModel — fast, no platform dependency
Assert.Equal("Hello", viewModel.Title);
Assert.True(viewModel.SaveCommand.CanExecute(null));

Mocking Strategy for MAUI Services

MAUI ServiceMock Strategy
ISecureStorageMock<ISecureStorage> — stub GetAsync/SetAsync
IPreferencesMock<IPreferences> — stub Get/Set/Remove
IConnectivityMock<IConnectivity> — return NetworkAccess
IGeolocationMock<IGeolocation> — return fixed Location
IFilePickerMock<IFilePicker> — return FileResult
IMediaPickerMock<IMediaPicker> — return FileResult
Shell navigationAbstract behind INavigationService
IDispatcherStub Dispatch to invoke action synchronously

Architecture: Interface-First for Testability

Define service interfaces so ViewModels have zero MAUI platform dependencies:

// ✅ These make your entire ViewModel layer testable
public interface INavigationService
{
    Task GoToAsync(string route);
    Task GoBackAsync();
}

public interface IDialogService
{
    Task<bool> ConfirmAsync(string title, string message);
}

Register implementations in MauiProgram.cs; inject interfaces into ViewModels.

Tips

  • No Application.Current or Shell.Current in ViewModels — wrap in injectable services
  • Use ObservableCollection<T> and [ObservableProperty] (MVVM Toolkit) for testable state
  • Assert on ViewModel properties and CanExecute, not UI bindings
  • Use TaskCompletionSource to test async waiting flows
  • Run dotnet test in CI to catch regressions early
  • On-device tests: Use xunit.runner.devices for real platform APIs (sensors, camera, Bluetooth)

Checklist

  • Test project targets plain net9.0/net10.0 (not platform-specific TFMs)
  • App project has conditional OutputType for test TFM
  • All MAUI static APIs wrapped behind injectable interfaces
  • ViewModels tested via properties and commands, not UI bindings
  • IDispatcher mocked to invoke synchronously in tests
  • dotnet test runs green in CI

Comments

Markdown supported