Lekcja 4. Testy jednostkowe w aplikacji mobilnej .NET MAUI
ŚredniPo 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.
- W rozwiązaniu z projektem .NET MAUI dodaj (jeśli jeszcze nie istnieje) bibliotekę
KalkulatorApp.Logikajak w lekcji 3: prawym przyciskiem na ROZWIĄZANIE → Dodaj → Nowy projekt → "Biblioteka klas (.NET)" → Utwórz. - Podepnij ją do projektu MAUI: prawym przyciskiem na projekt MAUI → Dodaj → Odwołanie do projektu… → zaznacz bibliotekę Logika → OK.
- Dodaj projekt testowy MSTest (Dodaj → Nowy projekt → "MSTest Test Project") z odwołaniem TYLKO do biblioteki Logika (nie do projektu MAUI).
- 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
Dodaj do
LogikaLokalizacjimetodę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).Zaprojektuj klasę
LogikaTerminarzaz 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).Zaprojektuj wspólną bibliotekę Logika dla prostej aplikacji "Preferencje użytkownika" (z lekcji o Preferences w dziale MAUI): klasa
WalidatorUstawiensprawdzają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.