Dataczwartek, 13 sierpnia 2026 Czas18:19:39
← Testowanie i dokumentowanie aplikacji

Lekcja 4. Testy jednostkowe w aplikacji mobilnej .NET MAUI

Średni

Po co się tego uczymy?

Częste pytanie uczniów: "czy telefon w ogóle da się testować automatycznie, skoro to co innego niż komputer?" Odpowiedź jest zaskakująco prosta: TAK, i to dokładnie w ten sam sposób co w WPF — bo testy jednostkowe nigdy nie testują "telefonu" ani "ekranu", tylko czystą logikę C#, która na obu platformach wygląda identycznie.

Teoria

Krok po kroku — ta sama biblioteka Logika, dwa projekty UI. Struktura jest identyczna jak w WPF (lekcja 3), z JEDNĄ ważną różnicą: biblioteka Logika musi być ZWYKŁĄ "Biblioteką klas (.NET)", NIE specjalną biblioteką klas .NET MAUI — dzięki temu projekt testowy MSTest może się do niej odwołać bez instalowania całego środowiska MAUI.

  1. W rozwiązaniu z projektem .NET MAUI dodaj (jeśli jeszcze nie istnieje) bibliotekę KalkulatorApp.Logika jak w lekcji 3: prawym przyciskiem na ROZWIĄZANIE → Dodaj → Nowy projekt → "Biblioteka klas (.NET)" → Utwórz.
  2. Podepnij ją do projektu MAUI: prawym przyciskiem na projekt MAUI → Dodaj → Odwołanie do projektu… → zaznacz bibliotekę Logika → OK.
  3. Dodaj projekt testowy MSTest (Dodaj → Nowy projekt → "MSTest Test Project") z odwołaniem TYLKO do biblioteki Logika (nie do projektu MAUI).
  4. Uruchom testy przez Test → Uruchom wszystkie testy — zauważ, że w ogóle NIE musisz uruchamiać emulatora Androida ani podłączać telefonu, żeby zobaczyć wynik testów.

Kluczowa teza tej lekcji: JEDEN zestaw testów działa zarówno dla WPF, jak i dla MAUI — pod warunkiem, że w obu projektach logika biznesowa została wydzielona do osobnej biblioteki klas (jak w lekcji 3), niezależnej od konkretnego frameworka UI. Testy MSTest "patrzą" wyłącznie na kod w tej bibliotece — nie wiedzą i nie muszą wiedzieć, czy nad nią stoi WPF (Windows), MAUI (Android/iOS) czy jakikolwiek inny interfejs.

Dlaczego to działa technicznie? Biblioteka klas z czystą logiką C# (bez odwołań do Microsoft.Maui ani do WPF) kompiluje się do zwykłej biblioteki .NET, uruchamialnej WSZĘDZIE, gdzie działa .NET — łącznie z projektem testowym uruchamianym na Twoim komputerze, BEZ potrzeby emulatora Androida czy prawdziwego telefonu. To ogromna oszczędność czasu: testy logiki wykonują się w ułamku sekundy, podczas gdy uruchomienie emulatora mobilnego trwa dziesiątki sekund.

Struktura projektu MAUI z warstwą testowalną — analogicznie do WPF: NazwaAplikacji (projekt .NET MAUI z widokami XAML, uruchamiany na urządzeniu/emulatorze), NazwaAplikacji.Logika (biblioteka klas, WSPÓLNA i identyczna niezależnie od platformy), NazwaAplikacji.Tests (projekt MSTest z odwołaniem do biblioteki Logika).

Co jest SPECYFICZNE dla MAUI, a co NIE podlega testom jednostkowym — funkcje takie jak GPS, powiadomienia lokalne czy dostęp do plików urządzenia (poznane w dziale Aplikacje mobilne) korzystają z API SPECYFICZNYCH dla platformy (Android/iOS) — tych NIE testuje się jednostkowo (wymagałoby to prawdziwego urządzenia lub emulatora — to już domena testów integracyjnych/manualnych). Natomiast logika, która PRZETWARZA dane z tych źródeł (np. "czy podana lokalizacja mieści się w promieniu 5 km od celu", "czy termin w terminarzu koliduje z innym") — TO JUŻ zwykła logika biznesowa, w pełni testowalna jednostkowo, tak jak każda inna metoda.

Walidacja wejścia w UI a czysta logika — jeśli pole formularza w MAUI (Entry) jest puste albo zawiera tekst zamiast liczby, walidację najlepiej wykonać w oddzielnym helperze (klasie pomocniczej), a nie bezpośrednio w code-behind strony — dzięki temu również walidacja staje się testowalna, mimo że jej WYNIK wyświetlany jest w interfejsie mobilnym.

Schemat

Rozwiązanie z DWOMA platformami UI, JEDNĄ wspólną logiką:

  KalkulatorApp.WPF/          KalkulatorApp.Maui/
  (Windows, XAML WPF)         (Android/iOS, XAML MAUI)
         │                            │
         └──────────┬─────────────────┘
                     ▼
       KalkulatorApp.Logika/     ← WSPÓLNA biblioteka klas
       LogikaKalkulatora.cs         (bez odwołań do WPF ani MAUI)
                     ▲
                     │
       KalkulatorApp.Tests/     ← TEN SAM zestaw testów MSTest
       LogikaKalkulatoraTests.cs   działa dla OBU platform UI

Przykład z życia

Wyobraź sobie aplikację bankową dostępną zarówno jako program na komputer (WPF), jak i aplikacja mobilna (MAUI) — reguła "opłata za przelew zagraniczny wynosi 1,5% kwoty, minimum 10 zł" MUSI działać IDENTYCZNIE na obu platformach. Firmy piszące taką logikę RAZ w wspólnej bibliotece i testując ją RAZ zestawem testów jednostkowych, mają pewność, że reguła jest spójna wszędzie — zamiast pisać (i osobno testować) tę samą logikę dwa razy, z ryzykiem rozjazdu między wersją desktopową a mobilną.

LogikaKalkulatora.cs + LogikaLokalizacji.cs (biblioteka Logika, wspólna WPF/MAUI)

// KalkulatorApp.Logika / LogikaKalkulatora.cs
// DOKŁADNIE TA SAMA klasa co w projekcie WPF (lekcja 3) -
// biblioteka jest WSPÓLNA i referencjonowana przez oba projekty UI.
using System;

namespace KalkulatorApp.Logika
{
    public class LogikaKalkulatora
    {
        public int Dodaj(int a, int b) => a + b;
        public int Odejmij(int a, int b) => a - b;

        public double Podziel(double a, double b)
        {
            if (b == ) throw new DivideByZeroException();
            return a / b;
        }

        public bool CzyDorosly(int wiek) => wiek >= 18;
    }
}

// Przykład logiki specyficznej dla MAUI, ale WCIĄŻ testowalnej -
// przetwarza dane z GPS (lekcja 21 działu MAUI), samo API GPS
// NIE jest tu testowane, tylko obliczenie odległości.
namespace KalkulatorApp.Logika
{
    public class LogikaLokalizacji
    {
        public double ObliczOdlegloscKm(double lat1, double lon1, double lat2, double lon2)
        {
            // uproszczony wzór (uczniowski, nie geodezyjny) do celów przykładu
            double dLat = lat2 - lat1;
            double dLon = lon2 - lon1;
            return Math.Sqrt(dLat * dLat + dLon * dLon) * 111; // ~111 km na stopień
        }

        public bool CzyWZasiegu(double odlegloscKm, double promienKm) =>
            odlegloscKm <= promienKm;
    }
}

LogikaLokalizacjiTests.cs (projekt testowy, uruchamiany BEZ emulatora)

// KalkulatorApp.Tests / LogikaLokalizacjiTests.cs
// Ten sam projekt testowy MSTest co dla WPF - dodatkowo testuje
// logikę specyficzną dla scenariuszy mobilnych (GPS), ale bez
// żadnego odwołania do prawdziwego GPS-u czy emulatora.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using KalkulatorApp.Logika;

namespace KalkulatorApp.Tests
{
    [TestClass]
    public class LogikaLokalizacjiTests
    {
        [TestMethod]
        public void CzyWZasiegu_OdlegloscMniejszaNizPromien_ZwracaTrue()
        {
            var logika = new LogikaLokalizacji();

            bool wynik = logika.CzyWZasiegu(odlegloscKm: 3.5, promienKm: 5.0);

            Assert.IsTrue(wynik);
        }

        [TestMethod]
        public void CzyWZasiegu_OdlegloscWiekszaNizPromien_ZwracaFalse()
        {
            var logika = new LogikaLokalizacji();

            bool wynik = logika.CzyWZasiegu(odlegloscKm: 8.0, promienKm: 5.0);

            Assert.IsFalse(wynik);
        }

        [TestMethod]
        public void CzyWZasiegu_OdlegloscRownaPromieniowi_ZwracaTrue()
        {
            // przypadek brzegowy: dokładnie na granicy zasięgu
            var logika = new LogikaLokalizacji();

            bool wynik = logika.CzyWZasiegu(odlegloscKm: 5.0, promienKm: 5.0);

            Assert.IsTrue(wynik);
        }
    }
}

Komentarz i wyjaśnienie kodu

LogikaKalkulatora jest tu skopiowana 1:1 z lekcji WPF — to NIE przypadek, tylko clou tej lekcji: skoro klasa nie zależy od żadnego frameworka UI, może być współdzielona (jako osobny projekt biblioteki) między aplikacją WPF i MAUI, a JEDEN zestaw testów sprawdza ją dla obu.

LogikaLokalizacji pokazuje ważne rozróżnienie: sam ODCZYT współrzędnych GPS wymaga prawdziwego urządzenia/emulatora i API Microsoft.Maui.Devices.Sensors.Geolocation (poznane w dziale MAUI) — TEGO nie testujemy jednostkowo. Ale OBLICZENIE, czy dwie współrzędne mieszczą się w zadanym promieniu, to zwykła matematyka w zwykłej metodzie C# — w pełni testowalna, bez potrzeby jakiegokolwiek urządzenia mobilnego.

CzyWZasiegu_OdlegloscRownaPromieniowi_ZwracaTrue to test PRZYPADKU BRZEGOWEGO — sprawdza zachowanie DOKŁADNIE na granicy warunku (<=), gdzie najczęściej ukrywają się błędy "o jeden" (użycie < zamiast <= dałoby tu błędny wynik false).

Ćwiczenie samodzielne

Weź dowolny projekt .NET MAUI z wcześniejszych lekcji działu Aplikacje mobilne (np. kalkulator BMI) i wydziel jego logikę obliczeniową do osobnej biblioteki klas, tak jak w przykładzie. Napisz dla niej projekt testowy MSTest i sprawdź, że testy uruchamiają się BEZ potrzeby emulatora Androida.

Zadania do pracy własnej

  1. Dodaj do LogikaLokalizacji metodę OpisStrefy(double odlegloscKm) zwracającą tekst: "blisko" (<1 km), "średnio" (1-5 km) lub "daleko" (>5 km) — i napisz test dla każdego z trzech przypadków oraz dla wartości granicznych (dokładnie 1 km i dokładnie 5 km).

  2. Zaprojektuj klasę LogikaTerminarza z metodą CzyKolidujeZTerminami(DateTime nowyTermin, List<DateTime> istniejaceTerminy, TimeSpan minimalnyOdstep) zwracającą true, jeśli nowy termin jest zbyt blisko (w czasie) jakiegoś istniejącego — napisz testy dla braku kolizji, kolizji i przypadku brzegowego (termin dokładnie na granicy minimalnego odstępu).

  3. Zaprojektuj wspólną bibliotekę Logika dla prostej aplikacji "Preferencje użytkownika" (z lekcji o Preferences w dziale MAUI): klasa WalidatorUstawien sprawdzająca, czy podane ustawienia (np. rozmiar czcionki 8-32, jasność 0-100%) mieszczą się w dozwolonych zakresach, wraz z kompletem testów obejmujących wartości poprawne, niepoprawne i graniczne dla każdego z ustawień.

Typowe błędy

Przekonanie, że "aplikacji mobilnej nie da się testować bez telefonu" — to prawda TYLKO dla warstwy UI i API specyficznych dla platformy (GPS, aparat, powiadomienia). Logika biznesowa, jeśli jest prawidłowo wydzielona, testuje się dokładnie tak samo jak w każdej innej aplikacji .NET, bez żadnego urządzenia.

Umieszczanie logiki obliczeniowej bezpośrednio w kodzie wywołującym API platformy (np. w tej samej metodzie, która odczytuje GPS) — uniemożliwia to przetestowanie samej logiki bez faktycznego dostępu do lokalizacji. Rozwiązanie: ODDZIEL pobranie danych (specyficzne dla platformy) od ich PRZETWORZENIA (uniwersalna logika, testowalna).

Duplikowanie tej samej logiki osobno w projekcie WPF i osobno w MAUI zamiast trzymania jej w jednej wspólnej bibliotece — prowadzi do rozjazdu (dwa miejsca do poprawienia przy każdej zmianie reguły biznesowej) i podwójnej pracy przy pisaniu testów.

Nawiązanie do egzaminu zawodowego

Ta lekcja bezpośrednio odpowiada na pytanie, czy testy jednostkowe mają sens w kontekście aplikacji mobilnych — odpowiedź brzmi: tak, dokładnie tak samo jak w WPF, o ile logika jest oddzielona od interfejsu. Temat testów jednostkowych (zarówno w kontekście desktopowym, jak i mobilnym) pojawiał się już na prawdziwym egzaminie INF.04. W kolejnej lekcji zobaczysz, jak analogiczne podejście (oddzielenie logiki od warstwy prezentacji) wygląda w aplikacji webowej opartej o Angular i ASP.NET Core.