Testování .NET MAUI aplikací v roce 2026: xUnit, Appium a strategie pro produkční kvalitu
Kompletní strategie testování .NET MAUI 10 v roce 2026: xUnit pro ViewModely s CommunityToolkit.Mvvm a Moq, Appium 5.x pro cross-platform UI testy, Page Object Model a integrace do GitHub Actions pipeline.
Testování .NET MAUI aplikací v roce 2026 stojí na dvou pilířích: xUnit pro unit a integration testy ViewModelů a Appium.WebDriver pro cross-platform UI automatizaci. Xamarin.UITest s MAUI totiž nefunguje a jeho oficiální náhradou je právě Appium ve spojení s knihovnou Plugin.Maui.UITestHelpers. V tomto průvodci projdeme kompletní strategii testování, kterou používáme v produkčních týmech: od nastavení testovacího projektu přes mockování služeb až po integraci UI testů do GitHub Actions pipeline.
xUnit je doporučený unit testovací framework pro .NET MAUI 10. Celý repozitář dotnet/maui na něj postupně migruje z NUnit.
Xamarin.UITest není s MAUI kompatibilní. Oficiální náhradou je Appium.WebDriver 5.x plus platform drivery (uiautomator2, xcuitest, mac2, windows).
ViewModely testujte proti čistému test hostu (net9.0/net10.0), ne proti platformním TFM, protože startup emulátoru zabíjí rychlost feedback loopu.
Plugin.Maui.UITestHelpers.Appium zásadně zjednodušuje migraci existujících Xamarin.UITest testů, protože mimikuje původní API.
Device runner testy s XHarness používejte jen tam, kde skutečně voláte platformní API (senzory, kamera, BLE).
V CI/CD spouštějte unit testy na každém commitu, UI testy nightly nebo pouze na release větvích, jejich náklad je řádově vyšší.
Proč je testování v .NET MAUI složitější než u webu
Když na standupu řeknu, že testování mobilní aplikace zabere víc práce než u backendu, kolegové z API teamu občas nevěří. Realita je taková, že mobilní testovací pipeline musí pokrýt fragmentované prostředí: různé verze Androidu (API 24 až 35), iOS (16 až 18), rozdílná rozlišení, výrobcem upravené shelly (Samsung One UI, Xiaomi HyperOS) a rozdíly mezi simulátorem a fyzickým zařízením. Web tento problém nemá. Máme čtyři prohlížeče a jednotný test hostitel.
Druhá vrstva složitosti přichází se samotnou architekturou MAUI. Framework staví na handlerech a platformních rendererech, takže i "sdílený" kód se za běhu chová jinak na iOS než na Androidu. V posledním projektu jsem tenhle bug řešil naostro: všechny xUnit testy prošly na CI (Linux runner), ale na iPhonu 15 padl CarouselView, protože iOS handler jinak počítá ItemsSource. Ponaučení? Unit testy vám dají rychlý feedback na business logiku, ale reálné chování UI musíte ověřit na skutečném platformním runtime.
Třetí faktor je rychlost feedback smyčky. Spuštění UI testu na iOS simulátoru trvá 15 až 30 sekund jen na inicializaci session, kdežto xUnit test na desktop hostu naběhne za milisekundy. To je jediný důvod, proč naše týmy dodržují pyramidový princip: většina testů má běžet mimo emulátor.
Pyramida testů pro mobilní aplikace
Pyramida testů pro .NET MAUI vypadá v produkčním týmu obvykle takto (počty jsou orientační pro středně velkou aplikaci s 30 až 50 obrazovkami):
Vrstva
Framework
Kde běží
Počet testů
Rychlost
Unit (ViewModel, services)
xUnit + Moq
Desktop test host (net10.0)
500–2000
< 10 s
Integration (DB, HttpClient)
xUnit + Testcontainers
Desktop test host
50–200
10–60 s
Device (platformní API)
xUnit + XHarness
Emulátor / zařízení
20–80
2–5 min
UI end-to-end
Appium + NUnit/xUnit
Emulátor / zařízení
30–120
10–40 min
Klíčové pravidlo, které v týmu vymáháme: každé business pravidlo je unit test. Pokud logiku pro výpočet slevy testujeme přes UI, děláme něco špatně. Flaky UI test si vezme dvě hodiny na debug jednoho večera, kdy release blokuje. Naopak UI test má potvrzovat, že komponenty jsou správně napojené a critical user journey (přihlášení, dashboard, checkout) funguje.
V mém posledním projektu jsme měli 1 400 unit testů (běh 8 sekund), 80 integration testů s in-memory SQLite (30 sekund) a 45 UI testů pokrývajících pět kritických flow (23 minut na Androidu, 31 minut na iOS). Tahle skladba nám drží PR merge cycle pod 15 minut a nightly UI regresní běh pod hodinou.
Jak nastavit unit testy s xUnit v .NET MAUI 10
Doporučujeme vytvořit samostatný test projekt, který cílí na net10.0 (nikoli na platformní TFM jako net10.0-android). Důvod je prostý: chceme, aby testy běžely v dotnet test na desktop hostu, ne na emulátoru. Pokud potřebujete platformní API, přesuňte tuto část kódu za interface a mockněte ji.
Klíčový trik: test projekt nesmí táhnout MAUI-specifické buildové targety. Do MyApp.Tests.csproj proto přidejte podmínku, která vyloučí platformní kompilaci:
Když MyApp.csproj cílí zároveň na net10.0-android;net10.0-ios;net10.0-maccatalyst, potřebujete v hlavním projektu odkaz podmínit, jinak restore selže. V test projektu použijte <ReferenceOutputAssembly>false</ReferenceOutputAssembly> a naopak nastavte vlastnost SetTargetFramework tak, aby referencovala jen desktop kód. Podrobný postup popisuje oficiální dokumentace Microsoft Learn k unit testování MAUI.
Testování ViewModelů s CommunityToolkit.Mvvm a Moq
Většina hodnoty testů leží ve ViewModelech, protože právě tam žijí business rozhodnutí (validace, orchestrace volání služeb, transformace dat pro binding). Pokud navrhujete ViewModel podle vzoru z našeho praktického průvodce MVVM s CommunityToolkit.Mvvm, testování bude přímočaré. Jen si dejte pozor na několik pravidel.
Pravidlo 1: Nesahejte na Application.Current ani Shell.Current
Obojí je statický singleton vázaný na běžící MAUI aplikaci, který v test hostu neexistuje. Místo toho vytvořte INavigationService a IDialogService a mockněte je. My tento pattern v týmu vynucujeme review checkem. Jakmile PR obsahuje Shell.Current, blokujeme merge.
Pravidlo 2: Testujte přes RelayCommand, ne přes UI event
CommunityToolkit.Mvvm generuje source-generated RelayCommand, který má vlastnost CanExecute. Testovat máte právě tyto commandy, tj. voláním Command.Execute(null) nebo await ViewModel.LoadAsync(), ne přes UI simulaci.
public class OrderListViewModelTests
{
private readonly Mock<IOrderService> _orderService = new();
private readonly Mock<IDialogService> _dialogService = new();
private readonly OrderListViewModel _sut;
public OrderListViewModelTests()
{
_sut = new OrderListViewModel(_orderService.Object, _dialogService.Object);
}
[Fact]
public async Task LoadAsync_When_ServiceReturnsOrders_Populates_Collection()
{
var orders = new[]
{
new Order(1, "Kávovar", 4_990m),
new Order(2, "Notebook", 32_500m)
};
_orderService
.Setup(s => s.GetOrdersAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(orders);
await _sut.LoadCommand.ExecuteAsync(null);
_sut.Orders.Should().HaveCount(2);
_sut.IsBusy.Should().BeFalse();
_sut.HasError.Should().BeFalse();
}
[Fact]
public async Task LoadAsync_When_ServiceThrows_SetsHasError_And_ShowsDialog()
{
_orderService
.Setup(s => s.GetOrdersAsync(It.IsAny<CancellationToken>()))
.ThrowsAsync(new HttpRequestException("Timeout"));
await _sut.LoadCommand.ExecuteAsync(null);
_sut.HasError.Should().BeTrue();
_dialogService.Verify(
d => d.ShowErrorAsync(It.IsAny<string>()),
Times.Once);
}
}
Všimněte si dvou detailů. Za prvé, používáme FluentAssertions. Čitelnost Should().HaveCount(2) versus Assert.Equal(2, _sut.Orders.Count) se v code review vyplatí. Za druhé, mock ověřujeme přes Verify(...Times.Once), což zachytí regresi typu "někdo omylem odstranil error dialog".
Co nahradilo Xamarin.UITest v .NET MAUI?
Krátká odpověď: Appium.WebDriver ve spojení s NUnit nebo xUnit, doplněný o knihovnu Plugin.Maui.UITestHelpers.Appium pro plynulou migraci. Xamarin.UITest jako produkt zamrzl s koncem podpory Xamarinu. V MAUI aplikaci ho už rozjet nedáte, protože byl svázaný s Calabash serverem, který v novém runtime není dostupný.
Ekosystém dnes nabízí čtyři reálné cesty:
Appium.WebDriver 5.x. De facto standard, doporučuje ho i Microsoft. Nejvíc dokumentace, největší komunita, běží na všech platformách včetně macOS Catalyst a Windows.
Maestro. YAML-based framework od Mobile.dev. Krásně jednoduchý zápis, ale slabší integrace s .NET runnerem a menší flexibilita pro custom asserty.
Maui.UITesting (Redth). Experimentální komunitní projekt s čistší integrací přímo do MAUI. Zajímavý pro sledování, ale ne produkčně zralý.
Waldo. SaaS s vizuální regresí, dobré pro netechnické tým členy, ale platíte za každý test run.
V produkčních týmech, které vedu, jedeme Appium jako defaultní volbu a Maestro doplňkově pro rychlé smoke testy. Důvod? Appium má Selenium-kompatibilní API, které naši QA inženýři už umí z webu. Detailní srovnání a migrační vodítko najdete v oficiálním .NET Blog článku o UI testování s Appium.
Kompletní nastavení Appium pro .NET MAUI
Setup má dvě části: instalaci Appium serveru (Node.js) a vytvoření .NET test projektů. Začneme serverem. Appium samo o sobě je Node.js aplikace, která spouští platformní drivery.
# Node.js LTS musí být nainstalováno (v20+)
npm install -g appium
# Drivery — instalujte jen ty, které skutečně potřebujete
appium driver install uiautomator2 # Android
appium driver install xcuitest # iOS (jen macOS host)
appium driver install mac2 # Mac Catalyst
appium driver install windows # Windows (WinAppDriver 1.2.1)
# Ověření
appium --version # 2.x
appium # spustí server na portu 4723
Pro každou platformu vytvořte samostatný test projekt (např. UITests.Android, UITests.iOS) a sdílený projekt UITests.Shared s testovacím kódem. Struktura, kterou v týmu používáme:
AppiumSetup.cs v platformním projektu inicializuje driver. Klíčové: musí sedět namespace se sdílenými testy, jinak NUnit [SetUpFixture] nezareaguje.
namespace UITests; // stejný namespace jako v UITests.Shared
[SetUpFixture]
public class AppiumSetup
{
private static AndroidDriver? _driver;
public static AppiumDriver Driver
=> _driver ?? throw new NullReferenceException("Driver není inicializován.");
[OneTimeSetUp]
public void RunBeforeAnyTests()
{
var serverUri = new Uri("http://127.0.0.1:4723");
var options = new AppiumOptions
{
AutomationName = "UiAutomator2",
PlatformName = "Android",
DeviceName = "Android Emulator",
App = Path.GetFullPath("../../../../MyApp/bin/Debug/net10.0-android/com.company.myapp-Signed.apk")
};
// package + activity musí sedět s [Register] atributem na MainActivity
options.AddAdditionalAppiumOption("appPackage", "com.company.myapp");
options.AddAdditionalAppiumOption("appActivity", "crc64...MainActivity");
_driver = new AndroidDriver(serverUri, options, TimeSpan.FromSeconds(180));
}
[OneTimeTearDown]
public void RunAfterAnyTests() => _driver?.Quit();
}
Page Object Model pro udržitelné UI testy
UI testy zestárnou rychleji než jakýkoli jiný kód, protože každá změna XAML může zlomit selector. Řešením je Page Object Model (POM). Každou obrazovku obalíte třídou, která exponuje business akce, a testy volají tyto akce místo By.Id(...).
public class LoginPage
{
private readonly AppiumDriver _driver;
public LoginPage(AppiumDriver driver) => _driver = driver;
private AppiumElement UsernameField
=> _driver.FindElement(MobileBy.AccessibilityId("UsernameEntry"));
private AppiumElement PasswordField
=> _driver.FindElement(MobileBy.AccessibilityId("PasswordEntry"));
private AppiumElement LoginButton
=> _driver.FindElement(MobileBy.AccessibilityId("LoginButton"));
public DashboardPage LoginAs(string username, string password)
{
UsernameField.SendKeys(username);
PasswordField.SendKeys(password);
LoginButton.Click();
return new DashboardPage(_driver);
}
}
[TestFixture]
public class LoginTests
{
[Test]
public void Login_With_ValidCredentials_Opens_Dashboard()
{
var loginPage = new LoginPage(AppiumSetup.Driver);
var dashboard = loginPage.LoginAs("[email protected]", "S3cure!");
Assert.That(dashboard.WelcomeText, Does.Contain("Vítejte"));
}
}
Používáme výhradně AccessibilityId jako selector. V XAML nastavujeme SemanticProperties.Description="LoginButton". Vedlejším efektem je, že jsme mimochodem dodělali základ accessibility podpory pro screenreadery.
Device runner testy s XHarness
Existuje třetí kategorie testů, kterou v pyramidě zmiňujeme méně, ale je nepostradatelná: device tests. Jsou to xUnit testy, které se spouštějí uvnitř procesu aplikace na skutečném zařízení nebo emulátoru. Používají se tam, kde potřebujete pravé platformní API (SecureStorage, Geolocation, Connectivity, přístup ke kameře) a mock by byl bezcenný.
Balíček xunit.runner.devices (nebo Microsoft.DotNet.XHarness.TestRunners.Xunit) obalí testy do MAUI aplikace, kterou pak spustíte přes XHarness CLI. Runner navíc vypublikuje výsledky ve formátu, který GitHub Actions umí zobrazit. Podobný přístup k testování platformních API popisujeme i v našem průvodci zabezpečením MAUI aplikací, kde jsme právě přes device runner ověřovali chování Secure Storage.
V praxi doporučujeme držet počet device testů nízko (20 až 80 na aplikaci) a soustředit je na tzv. platform integration boundary, tedy místa, kde váš kód volá nativní SDK.
Integrace testů do CI/CD pipeline
V GitHub Actions rozdělujeme workflow na tři joby: unit běží na Ubuntu (nejlevnější), ui-android na Ubuntu s KVM emulátorem a ui-ios na macOS runneru. Unit testy pouštíme na každém push, UI testy na main větvi a nightly.
Několik detailů, které nám ušetřily hodiny debugování. Za prvé: APK musí být buildnuté před UI testem, protože Appium očekává hotový binární balíček. Za druhé: emulátor nastartujte přes akci reactivecircus/android-emulator-runner, která správně nakonfiguruje KVM na Ubuntu runneru. Bez toho vám emulátor běží v software renderingu a je 10× pomalejší. Za třetí: iOS UI testy vyžadují macOS runner (~10× dražší než Ubuntu), proto je spouštíme jen nightly.
Pokud jste právě uprostřed přechodu ze starého stacku, doporučuji podívat se i na naši migrační příručku z Xamarin.Forms na .NET MAUI. Testovací pipeline je jedna z věcí, kterou tam explicitně přebudováváme, ne jen "portujeme".
Časté chyby a jak jim předejít
Rekapitulace z posledních čtyř MAUI projektů, které jsme dovedli do produkce:
Testování statické UI třídy místo ViewModelu. Když testujete přes ContentPage, testujete zároveň i binding engine, což je pomalé a fragilní. Držte se čistého ViewModel testu.
Chybějící AutomationId nebo SemanticProperties.Description. Appium bez identifikátoru hledá elementy přes XPath, což je pomalé a náchylné na změny. Vždy definujte accessibility identifier v XAML.
Sdílený stav mezi UI testy. Pokud test A vytvoří objednávku a test B ji smaže, pořadí testů začne mít význam. Každý UI test má začínat čistým stavem, buď restart aplikace, nebo teardown na backendu.
Mockování HttpClient přímo. Používejte IHttpClientFactory nebo HttpMessageHandler mock. Přímý mock HttpClient nefunguje spolehlivě kvůli sealed metodám.
Ignorování flaky testů. Když test padne 1 z 20 běhů, není "občas flaky". Je rozbitý. Buď najděte race condition, nebo test odstraňte. Ignorovaný flaky test korumpuje důvěru v celou sadu.
Často kladené otázky
Jaký test framework je nejlepší pro .NET MAUI?
xUnit je aktuálním doporučeným standardem pro nové .NET MAUI projekty a používá ho i samotný repozitář dotnet/maui. NUnit dává smysl jen pokud migrujete stávající testovou sadu nebo píšete Appium UI testy, tam má NUnit s [SetUpFixture] historicky lepší ergonomii.
Funguje Xamarin.UITest s .NET MAUI?
Ne. Xamarin.UITest není s .NET MAUI kompatibilní, protože byl svázaný s Calabash serverem, který v novém runtime chybí. Oficiální migrační cesta vede na Appium.WebDriver 5.x, ideálně s pomocnou knihovnou Plugin.Maui.UITestHelpers.Appium, která mimikuje původní API.
Jak nastavím Appium pro .NET MAUI na Windows?
Nainstalujte Node.js LTS, spusťte npm install -g appium a nainstalujte Windows driver příkazem appium driver install windows. Dále potřebujete WinAppDriver 1.2.1 (stažený z GitHub releases) a v test projektu nastavte AppiumOptions s PlatformName = "Windows" a cestu k .exe souboru vaší MAUI aplikace.
Jak testovat ViewModely, když závisí na Shell.Current?
Nezávisejte na Shell.Current přímo. Vytvořte INavigationService s metodami jako GoToAsync(string route), implementaci nasměřujte na Shell.Current.GoToAsync jen v produkčním kódu a v testech mockněte přes Moq. Stejný pattern použijte pro Application.Current, Preferences a další statické singletony.
Kolik UI testů má mít mobilní aplikace?
Doporučujeme 30 až 120 UI testů pro středně velkou aplikaci, což pokryje 5 až 10 kritických user journey. Většina testovací sady (obvykle 70 až 80 %) by měla být unit testy, protože UI testy jsou 100× pomalejší a náchylnější k flakiness. Cílem UI testů je ověřit, že komponenty jsou správně napojené a critical paths fungují, ne testovat business logiku.
Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.