Ažurirano: 29. kolovoza 2026.
Testiranje .NET MAUI aplikacija u 2026. najbolje se rješava kombinacijom tri sloja: unit testovi za ViewModele i servise (xUnit ili NUnit, uz Moq/NSubstitute za mockiranje), UI testovi preko Appium 2 za scenarije s pravim ekranom, te device testovi iz Microsoft.Maui.TestUtils.DeviceTests za handler i renderer razinu. Ovaj vodič pokazuje minimalni skup projekata koji stvarno vrijedi držati u repozitoriju, s primjerima koda za .NET 9 i .NET 10 te konfiguracijom za GitHub Actions.
Zašto je testiranje .NET MAUI aplikacija drugačije?
Testiranje MAUI aplikacije se razlikuje od klasičnog ASP.NET Core projekta iz jednog jednostavnog razloga: velik dio koda ovisi o MauiProgram kontekstu, o platform-specific inicijalizaciji i o UI dispatcheru koji ne postoji izvan uređaja. Kad sam prvi put pokušala pokrenuti test koji instancira Application.Current.MainPage u čistom .NET 9 test hostu, dobila sam InvalidOperationException: You must call Microsoft.Maui.Controls.Compatibility.Forms.Init(). To više nije tako, ali princip ostaje: direktan poziv u Microsoft.Maui.Controls namespace iz unit testa je crvena zastava.
Otud dolazi arhitektura testiranja koja je za MAUI stvarno održiva: kod dijelite u tri sloja i za svaki koristite drugačiju vrstu testa. Poslovna logika i ViewModeli žive u zasebnom razrednom projektu bez MAUI referenci i pokrivaju se brzim in-process unit testovima. Handleri, custom kontrole i renderer kod žive u MAUI projektu i testiraju se na uređaju putem device test runnera. Kompletni end-to-end scenariji (login, checkout, offline sync) pokrivaju se Appium testovima koji voze pravu buildanu aplikaciju. Ova podjela nije akademska, ona određuje koliko će vam CI trajati. Solidan unit test suite od 800 testova mora završiti ispod jedne minute; UI suite od 30 scenarija realno traje 15–25 minuta na macOS runneru.
Kako postaviti unit test projekt za .NET MAUI?
Unit test projekt za MAUI je običan xUnit ili NUnit projekt s ciljanim frameworkom net9.0 ili net10.0 (bez -android, -ios ili -windows sufiksa). Ključno pravilo: ne referencira Microsoft.Maui.Controls. Ako vam se u testovima pojavi potreba za tim namespaceom, to je znak da testirate previše i da je logika trebala biti izvučena u posebnu klasu.
Struktura koju koristim u produkciji izgleda ovako. Glavni MAUI projekt MyApp ovisi o MyApp.Core (POCO modeli, servisi, ViewModeli). Test projekt MyApp.Core.Tests referencira samo MyApp.Core. Ovo razdvajanje je najbolja stvar koju možete napraviti za testabilnost:
<!-- MyApp.Core.Tests.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.v3" Version="1.0.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.*" />
<PackageReference Include="Moq" Version="4.20.*" />
<PackageReference Include="FluentAssertions" Version="6.12.*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyApp.Core\MyApp.Core.csproj" />
</ItemGroup>
</Project>
xUnit v3 je preporučeni izbor od 2025. jer donosi izolaciju po test klasi (svaka klasa dobiva vlastiti AssemblyLoadContext), što uklanja cijeli razred flaky testova zbog statičkog stanja. Ako imate stari test suite u xUnit v2, migracija je uglavnom pod-jedan-dan posao jer je API kompatibilan. Za bezbroj postojećih Xamarin.UITest projekata NUnit ostaje pragmatičan izbor, pa ga nemojte mijenjati bez razloga.
Testiranje ViewModela s CommunityToolkit.Mvvm
Ako koristite CommunityToolkit.Mvvm generatore, ViewModel se testira kao obična klasa, bez ikakvog UI konteksta. Primjer: ViewModel koji dohvaća listu narudžbi i prikazuje loading state.
public partial class OrdersViewModel : ObservableObject
{
private readonly IOrderService _orderService;
public OrdersViewModel(IOrderService orderService)
{
_orderService = orderService;
}
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private ObservableCollection<Order> orders = new();
[ObservableProperty]
private string? errorMessage;
[RelayCommand]
private async Task LoadAsync()
{
IsLoading = true;
ErrorMessage = null;
try
{
var result = await _orderService.GetOrdersAsync();
Orders = new ObservableCollection<Order>(result);
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
finally
{
IsLoading = false;
}
}
}
Test pokriva tri stvari: da IsLoading ide kroz true → false, da se lista popuni, i da se greška prevede u ErrorMessage. Nema MainThread.BeginInvokeOnMainThread mocka, nema Application.Current, nema MessagingCenter. Ako toga ima u vašem ViewModelu, izvucite ga iza sučelja prije nego pišete testove.
public class OrdersViewModelTests
{
[Fact]
public async Task LoadAsync_PopulatesOrders_WhenServiceReturnsData()
{
var mock = new Mock<IOrderService>();
mock.Setup(s => s.GetOrdersAsync())
.ReturnsAsync(new List<Order> { new(1, "Test") });
var vm = new OrdersViewModel(mock.Object);
await vm.LoadCommand.ExecuteAsync(null);
vm.Orders.Should().HaveCount(1);
vm.IsLoading.Should().BeFalse();
vm.ErrorMessage.Should().BeNull();
}
[Fact]
public async Task LoadAsync_SetsErrorMessage_WhenServiceThrows()
{
var mock = new Mock<IOrderService>();
mock.Setup(s => s.GetOrdersAsync())
.ThrowsAsync(new HttpRequestException("Network down"));
var vm = new OrdersViewModel(mock.Object);
await vm.LoadCommand.ExecuteAsync(null);
vm.ErrorMessage.Should().Be("Network down");
vm.IsLoading.Should().BeFalse();
}
}
Mockiranje ovisnosti: Moq, NSubstitute i vlastiti fakeovi
Moq je i dalje najkorišteniji mocking framework u .NET ekosustavu, ali od kontroverze oko SponsorLink telemetrije 2023. ekipe češće biraju NSubstitute (čišći API) ili pišu vlastite fake implementacije. Moje pravilo: za servise s više od dvije-tri metode koristim Moq ili NSubstitute; za jednostavne interfaceove pišem FakeOrderService klasu ručno. Ručni fake nikada ne postane bottleneck, čita se bolje u code reviewu i preživljava refactor.
NSubstitute primjer za istu situaciju izgleda ovako:
var service = Substitute.For<IOrderService>();
service.GetOrdersAsync().Returns(new List<Order> { new(1, "Test") });
var vm = new OrdersViewModel(service);
await vm.LoadCommand.ExecuteAsync(null);
await service.Received(1).GetOrdersAsync();
vm.Orders.Should().HaveCount(1);
Za HTTP servise koje testirate integracijski, ne mockirajte HttpClient. Umjesto toga podignite WireMock.Net instancu koja glumi backend. Testovi ostaju in-process i brzi (nema mreže), a hvatate greške u parsiranju odgovora koje mock nikada neće hvatati jer mock vraća već napravljene objekte.
UI testiranje .NET MAUI aplikacija s Appium 2
Xamarin.UITest je službeno u maintenance modu i nije preporučen za nove MAUI projekte. Prava opcija je Appium 2, koji je multi-platformski driver s .NET klijentom kroz Appium.WebDriver NuGet paket. Prednost je da isti testovi voze i iOS i Android buildove, a Appium Inspector pomaže pri pronalaženju locatora.
Minimalni setup uključuje Appium server (Node.js paket) i UiAutomator2 driver za Android odnosno XCUITest driver za iOS. Pokretanje lokalno:
# Instalacija Appium 2 i drivera
npm install -g appium@2
appium driver install uiautomator2
appium driver install xcuitest
# Pokretanje servera
appium --port 4723
Test u xUnit obliku koji provjerava login flow izgleda ovako:
public class LoginUiTests : IDisposable
{
private readonly AndroidDriver _driver;
public LoginUiTests()
{
var options = new AppiumOptions
{
PlatformName = "Android",
AutomationName = "UiAutomator2",
App = "/path/to/com.myapp.debug.apk",
DeviceName = "Pixel_7_API_34"
};
_driver = new AndroidDriver(new Uri("http://127.0.0.1:4723"), options);
_driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
[Fact]
public void SuccessfulLogin_NavigatesToHome()
{
_driver.FindElement(MobileBy.AccessibilityId("emailEntry"))
.SendKeys("test@example.com");
_driver.FindElement(MobileBy.AccessibilityId("passwordEntry"))
.SendKeys("Passw0rd!");
_driver.FindElement(MobileBy.AccessibilityId("loginButton")).Click();
var welcome = _driver.FindElement(MobileBy.AccessibilityId("welcomeLabel"));
welcome.Text.Should().StartWith("Dobrodošli");
}
public void Dispose() => _driver.Quit();
}
Ključna praksa: uvijek koristite AutomationId na kontrolama koje testirate. U MAUI XAML-u to je jednostavno: <Entry AutomationId="emailEntry" />. Bez toga se locatori vežu na XPath koji puca čim netko promijeni layout. Naši locatori u produkciji su isključivo MobileBy.AccessibilityId, sve ostalo je flaky.
Device testovi za custom handlere i platform-specific kod
Ako pišete custom handlere ili mappere, unit testovi ne pomažu jer testirate ponašanje na pravom UIView odnosno Android.Views.View objektu. Microsoft je otvoreno podijelio isti test infrastrukturu koju koristi za dotnet/maui repozitorij: paket Microsoft.Maui.TestUtils.DeviceTests.Runners pokreće xUnit runner unutar MAUI aplikacije koja se instalira na uređaj, izvršava testove, i vraća rezultate u XML.
Postavka je nekonvencionalna: kreirate novi MAUI aplikacijski projekt (ne razredni), dodate NuGet pakete Microsoft.Maui.TestUtils.DeviceTests.Runners i Microsoft.Maui.TestUtils.DeviceTests, i u MauiProgram.cs pozovete builder.ConfigureTests(new TestOptions { Assemblies = { typeof(MyHandlerTests).Assembly } }). Testovi izgledaju kao obični xUnit:
[Category(TestCategory.Handler)]
public class BadgeViewHandlerTests : HandlerTestBase<BadgeViewHandler, BadgeView>
{
[Fact]
public async Task InitialBadgeCount_RendersCorrectValue()
{
var view = new BadgeView { Count = 5 };
await InvokeOnMainThreadAsync(() =>
{
var handler = CreateHandler(view);
var nativeText = GetNativeBadgeText(handler);
Assert.Equal("5", nativeText);
});
}
}
Ovo je zlato kad radite na ObjC ili Java.Lang stranicima handlera — u našem MAUI portu iz Xamarin.Forms upravo su ovi testovi uhvatili tri regresije koje UI testovi nisu jer se problem događao samo pri određenoj sekvenci MapCount i MapColor poziva.
Integracija testova u CI/CD pipeline
Unit testovi su trivijalni u GitHub Actions pipeline za MAUI: bilo koji ubuntu-latest runner s dotnet-version: '10.0.x' vrti dotnet test. UI i device testovi zahtijevaju macOS runner za iOS i emulator akciju za Android:
name: MAUI Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- run: dotnet test tests/MyApp.Core.Tests
--logger "trx;LogFileName=results.trx"
--collect:"XPlat Code Coverage"
android-ui-tests:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Install MAUI workload
run: dotnet workload install maui-android
- name: Build APK
run: dotnet publish src/MyApp -f net10.0-android -c Release
- name: Setup Node and Appium
uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm install -g appium@2 && appium driver install uiautomator2
- name: Start Android emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
script: |
appium --port 4723 &
dotnet test tests/MyApp.UITests
Rezultati se agregiraju kroz trx logger i objave u PR check preko dorny/test-reporter akcije. Za device testove Microsoft koristi Appium.WebDriver uz Test Cloud alternative kao BrowserStack ili Sauce Labs. Ako vam matrica uređaja raste iznad 3–4 kombinacije, cloud farma se brzo isplati.
Snapshot testovi i vizualna regresija
Vizualna regresija (usporedba screenshota) je najlakši sloj za dodati i najteži za održati. .NET MAUI 10 uvodi eksperimentalnu VisualDiff infrastrukturu unutar Microsoft.Maui.TestUtils.DeviceTests, ali API je još podložan promjenama. Za produkcijsku upotrebu preporučam Verify.Xunit paket koji radi baseline snapshot i storniran diff prilikom prve neuspješne provjere.
Realna upotreba: snapshot ne koristim za piksel-perfect UI (pucne pri svakoj promjeni fonta ili emulator verzije). Koristim ga za JSON kontrakte servisa, DTO mapiranje i za XAML rendered output serijaliziran u strukturu:
[Fact]
public Task OrderSummary_MatchesExpectedShape()
{
var vm = new OrderSummaryViewModel(new FakeOrderService());
vm.Load(orderId: 42);
return Verify(new
{
vm.OrderNumber,
vm.TotalAmount,
Items = vm.Items.Select(i => new { i.Name, i.Quantity })
});
}
Kad promijenite shemu namjerno, obrišete .received.txt datoteku i test prihvati novi baseline. Ovo je najbrži put do 80% pokrivenosti za view modele s velikim mapiranjem podataka.
Najčešće greške pri testiranju MAUI aplikacija
Iz revizija koda i test suiteova koje sam vidjela u zadnjih 18 mjeseci, evo obrazaca koji se ponavljaju:
- Testiranje kroz
Application.Current. Bilo koji test koji poseže zaApplication.Current.MainPageili navigira prekoShell.Current.GoToAsyncpuknut će u čistom test hostu. Umjesto toga apstrahirajte navigaciju krozINavigationServicesučelje. - Sinkroni
Task.Result. U testovima koji koristeasynccommande, nikada ne pozivajte.Resultili.Wait(). Testovi će raditi na .NET 8, deadlockati na iOS device runneru i vratiti se u pull request kao "flaky test" tickete. - Nedostatak izolacije u xUnit v2.
ITestOutputHelperse ne dijeli između testova ali statičke kolekcije se dijele. Migrirajte na xUnit v3 ili koristiteXunit.Extensions.AssemblyFixtureza per-klasu izolaciju. - UI locatori bazirani na tekstu.
MobileBy.XPath("//*[text()='Prijava']")je razlog #1 zašto lokalizirane aplikacije imaju flaky UI testove. UvijekAccessibilityId. - Testovi bez trimming provjere. Kod koji radi u Debug puca kad iOS Release build napravi AOT trimming jer reflekcija ostaje bez metadata. Barem jedan smoke test u pipelineu mora biti izvršen na
-c ReleaseAPK/IPA-u. - Ignoriranje test rezultata iz
net10.0-androidtargeta.dotnet testpo defaultu preskače multi-targeted projekte s mobilnim frameworkovima. Eksplicitno navedite-f net10.0za unit testove ili razdvojite projekte.
Većina ovih grešaka nije bug u MAUI-ju — to su navike prenesene iz Xamarin.Forms ili ASP.NET Core svijeta koje se u novom kontekstu urušavaju. Za dodatno štivo pogledajte službenu .NET MAUI dokumentaciju i TestUtils direktorij u dotnet/maui repozitoriju gdje Microsoft otvoreno demonstrira vlastite obrasce testiranja.
Često postavljana pitanja
Radi li Xamarin.UITest s .NET MAUI aplikacijama?
Xamarin.UITest je službeno u maintenance modu i ne preporučuje se za nove MAUI projekte. Radi u ograničenom scenariju s .NET 8 Android buildovima, ali za nove testove koristite Appium 2 s Appium.WebDriver NuGet paketom.
Koliko brzo bi unit test suite za MAUI aplikaciju trebao završiti?
Solidan unit test suite od 500–1000 testova (ViewModeli i servisi bez UI-ja) mora završiti ispod jedne minute na modernom laptopu. Ako ide sporije, obično je uzrok mrežni poziv ili file I/O koji nije mockiran.
Mogu li pokretati iOS UI testove bez macOS runnera?
Ne za lokalne buildove, jer XCUITest driveru trebaju Xcode simulator i Apple potpisivanje, što je moguće samo na macOS-u. Alternativa je BrowserStack ili Sauce Labs koji uzimaju vaš IPA i pokreću ga u cloud farmi.
Kako se testira MessagingCenter ili WeakReferenceMessenger komunikacija?
Umjesto direktnog testiranja messengera, injektirajte IMessenger sučelje (iz CommunityToolkit.Mvvm) u ViewModel i mockirajte ga. Provjerite da je Send<T>() pozvan s očekivanom porukom kroz mock.Verify.
Koje su alternative Moq mocking framework-u u 2026?
Glavne alternative su NSubstitute (čišći sintaksni API, aktivno održavan), FakeItEasy (bogatiji konfiguracijski model) i ručno pisani fake razredi za jednostavne servise. Sva tri rade s .NET 9 i .NET 10 bez telemetrije.
