Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
davidortinau avatar

Maui Unit Testing

  • 47 installs
  • 163 repo stars
  • Updated July 6, 2026
  • davidortinau/maui-skills

Provides xUnit testing guidance for .NET MAUI apps including ViewModel testing, mocking MAUI services, test project setup, and code coverage.

About

Guides xUnit testing for .NET MAUI apps covering ViewModel testing, mocking MAUI services, test project setup, code coverage and on-device test runners. A developer uses it when writing unit tests for a MAUI app.

  • ViewModel testing and mocking MAUI services with xUnit
  • Test project setup, code coverage, and on-device runners

Maui Unit Testing by the numbers

  • 47 all-time installs (skills.sh)
  • Ranked #1,237 of 2,153 Testing & QA skills by installs in the Skillselion catalog
  • Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidortinau/maui-skills --skill maui-unit-testing

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs47
repo stars163
Last updatedJuly 6, 2026
Repositorydavidortinau/maui-skills

What it does

Provides xUnit testing guidance for .NET MAUI apps including ViewModel testing, mocking MAUI services, test project setup, and code coverage.

Files

SKILL.mdMarkdownGitHub ↗

.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.CurrentINavigationService
  • Application.Current → avoid entirely
  • SecureStorage.DefaultISecureStorage
  • Connectivity.CurrentIConnectivity

❌ 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

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.