Testiranje .NET MAUI aplikacija: Unit testovi, UI testovi i Appium (2026)

Praktični vodič za testiranje .NET MAUI aplikacija u 2026. Pokriva unit testove s xUnit, UI automatizaciju s Appium 2, device testove za custom handlere i konfiguraciju GitHub Actions pipelinea za iOS i Android buildove.

Testiranje .NET MAUI: Unit + Appium (2026)

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.

  • Držite ViewModel i servisni sloj bez direktnih referenci na Microsoft.Maui.Controls. Tada se testiraju kao običan .NET razredni projekt, bez emulatora.
  • Za unit testove koristite xUnit v3 ili NUnit 4; oba rade s .NET 9/10 i podržavaju paralelno izvršavanje po test klasi.
  • UI testove pišite u Appium 2 s Appium.WebDriver NuGet paketom, jer je Xamarin.UITest službeno napušten s .NET 8 tranzicijom.
  • Handler i platform-specific kod testira se Microsoft.Maui.TestUtils.DeviceTests.Runners paketom koji pokreće xUnit na pravom uređaju.
  • CI/CD za MAUI testove zahtijeva macos-14 runnere za iOS i Android emulator akciju za Android; Windows runneri pokreću samo Windows i unit testove.
  • Snapshot testiranje slika (VisualDiff) ostaje eksperimentalno u .NET MAUI 10, pa ga koristite za regresije, ne kao primarni sloj.

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("[email protected]");
        _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 za Application.Current.MainPage ili navigira preko Shell.Current.GoToAsync puknut će u čistom test hostu. Umjesto toga apstrahirajte navigaciju kroz INavigationService sučelje.
  • Sinkroni Task.Result. U testovima koji koriste async commande, nikada ne pozivajte .Result ili .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. ITestOutputHelper se ne dijeli između testova ali statičke kolekcije se dijele. Migrirajte na xUnit v3 ili koristite Xunit.Extensions.AssemblyFixture za per-klasu izolaciju.
  • UI locatori bazirani na tekstu. MobileBy.XPath("//*[text()='Prijava']") je razlog #1 zašto lokalizirane aplikacije imaju flaky UI testove. Uvijek AccessibilityId.
  • 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 Release APK/IPA-u.
  • Ignoriranje test rezultata iz net10.0-android targeta. dotnet test po defaultu preskače multi-targeted projekte s mobilnim frameworkovima. Eksplicitno navedite -f net10.0 za 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.

O Autoru Devika Ramaswamy

Devika spent four years on the Xamarin team at Microsoft before the transition to .NET MAUI, where she worked on the iOS handler layer and shipped fixes that landed in the .NET 7 and .NET 8 release notes. She left Redmond in 2023 to run mobile engineering at a Series B logistics startup, porting their 600k-line Xamarin.Forms codebase to MAUI over eleven months. She writes mostly about the unglamorous parts of cross-platform work: handler internals, AOT trimming on iOS, MSBuild target customization, and why your hot reload keeps breaking. She holds the .NET MAUI MVP award (2024, 2025) and has spoken at .NET Conf and Xamarin Expert Day. Based in Bengaluru, she still pushes the occasional PR to the dotnet/maui repo on weekends.