Dataniedziela, 9 sierpnia 2026 Czas07:56:04
← Testowanie i dokumentowanie aplikacji

Lekcja 3. Testy jednostkowe w aplikacji desktopowej WPF

Średni

Po co się tego uczymy?

Aplikacja WPF ma okna, przyciski i pola tekstowe — a testów jednostkowych NIE piszemy dla wyglądu interfejsu. Sekret polega na tym, żeby logikę biznesową (obliczenia, walidację, reguły) trzymać w OSOBNEJ warstwie, niezależnej od okien — dokładnie tak, jak robi to wzorzec MVVM poznany wcześniej w dziale WPF. Dzięki temu możesz przetestować "mózg" aplikacji dokładnie tym samym MSTestem, którego użyłeś w lekcji 2, mimo że tym razem aplikacja ma pełny graficzny interfejs.

Teoria

Krok po kroku — budowa rozwiązania z trzema projektami:

  1. Otwórz istniejący projekt WPF (albo utwórz nowy jak w lekcji o pierwszej aplikacji WPF, dział Aplikacje desktopowe).
  2. Dodaj bibliotekę logiki: prawym przyciskiem na ROZWIĄZANIE → Dodaj → Nowy projekt → wyszukaj Class Library → wybierz "Biblioteka klas (.NET)" → nazwij np. KalkulatorApp.Logika → Utwórz.
  3. Podepnij bibliotekę do aplikacji WPF: prawym przyciskiem na projekt WPF → Dodaj → Odwołanie do projektu… → zaznacz KalkulatorApp.Logika → OK.
  4. Dodaj projekt testowy: prawym przyciskiem na ROZWIĄZANIE → Dodaj → Nowy projekt → wyszukaj MSTest → nazwij np. KalkulatorApp.Tests → Utwórz.
  5. Podepnij projekt testowy WYŁĄCZNIE do biblioteki Logika (nie do WPF!): prawym przyciskiem na KalkulatorApp.Tests → Dodaj → Odwołanie do projektu… → zaznacz KalkulatorApp.Logika → OK.
  6. Przenieś (albo od razu napisz) klasy z logiką biznesową w projekcie Logika, testy w projekcie Tests, uruchom przez Test → Uruchom wszystkie testy.

Kluczowa zasada: testujemy Logikę, nie UI. Okna, przyciski, kolory, style — to wygląd, nie zachowanie, i nie da się (ani nie warto) tego sensownie testować jednostkowo. Natomiast KAŻDA reguła biznesowa (obliczenia, walidacja, decyzje "co się stanie, gdy...") powinna dać się przetestować BEZ uruchamiania jakiegokolwiek okna.

Jak to osiągnąć — oddzielenie Logiki od UI (przypomnienie z wzorca MVVM). Cała logika biznesowa trafia do zwykłych klas C# (np. w folderze/projekcie Logika/), które NIE mają żadnego odwołania do WPF, okien czy kontrolek. ViewModel korzysta z tych klas i WYSTAWIA ich wyniki do UI przez bindowanie, ale sam nie zawiera skomplikowanej logiki — tylko "klej" łączący Logikę z Widokiem.

Co można testować w takiej architekturze:

  • Metody zwracające wartość — czy wynik jest poprawny (np. obliczenia kalkulatora).
  • Metody obliczeniowe — wszelkie przeliczenia, logika biznesowa.
  • Walidację danych — czy metoda poprawnie wykrywa błędne dane wejściowe.
  • Funkcje rzucające wyjątek — np. czy dzielenie przez zero rzuca DivideByZeroException.
  • Metody typu true/false — np. logika sprawdzająca hasło, wiek, uprawnienia.

Struktura projektu z warstwą testowalną. Typowe rozwiązanie w Visual Studio: trzy projekty w jednym rozwiązaniu — NazwaAplikacji (właściwa aplikacja WPF z oknami XAML), NazwaAplikacji.Logika (biblioteka klas z czystą logiką, bez zależności od WPF) i NazwaAplikacji.Tests (projekt MSTest z odwołaniem TYLKO do biblioteki Logika — nie musi w ogóle "widzieć" WPF).

Dodatkowe polecenia MSTest — dane parametryczne. [DataTestMethod] razem z [DataRow(...)] pozwala uruchomić JEDEN test z WIELOMA zestawami danych wejściowych, zamiast kopiować niemal identyczny test kilka razy — np. sprawdzenie tej samej metody dla kilku różnych par liczb.

Schemat

Rozwiązanie WPF (Solution)
 ├── KalkulatorApp/                (WPF: okna XAML, MainWindow.xaml.cs)
 │     └── odwołuje się do ↓
 ├── KalkulatorApp.Logika/         (biblioteka klas, BEZ odwołania do WPF)
 │     └── LogikaKalkulatora.cs
 └── KalkulatorApp.Tests/          (MSTest, odwołanie TYLKO do Logika)
       └── LogikaKalkulatoraTests.cs

  MainWindow.xaml  ──bindowanie──▶  ViewModel  ──wywołuje──▶  LogikaKalkulatora
       (UI, NIE testujemy)      (cienki "klej")      (logika, TESTUJEMY tutaj)

Przykład z życia

Wyobraź sobie kalkulator: dziś działa poprawnie, ale jutro dopiszesz nową funkcję i nagle 2+2 daje 5. Bez testów zauważysz to dopiero, gdy ktoś zgłosi błąd — z testami dowiesz się o tym NATYCHMIAST po zmianie kodu, jeszcze zanim ktokolwiek uruchomi aplikację. To dokładnie ta sama korzyść, którą testy dają profesjonalnym zespołom programistycznym: możliwość zmiany kodu BEZ STRACHU, że coś po cichu przestało działać.

LogikaKalkulatora.cs (biblioteka Logika, bez WPF)

// KalkulatorApp.Logika / LogikaKalkulatora.cs
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;
    }
}

LogikaKalkulatoraTests.cs (projekt testowy)

// KalkulatorApp.Tests / LogikaKalkulatoraTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using KalkulatorApp.Logika;

namespace KalkulatorApp.Tests
{
    [TestClass]
    public class LogikaKalkulatoraTests
    {
        [TestMethod]
        public void Dodaj_DwaPlusTrzy_ZwracaPiec()
        {
            var logika = new LogikaKalkulatora();
            int wynik = logika.Dodaj(2, 3);

            Assert.AreEqual(5, wynik);
        }

        [TestMethod]
        public void Podziel_PrzezZero_RzucaWyjatek()
        {
            var logika = new LogikaKalkulatora();

            Assert.ThrowsException<DivideByZeroException>(() => logika.Podziel(4, ));
        }

        // [DataTestMethod] + [DataRow] - jeden test, wiele zestawów danych
        [DataTestMethod]
        [DataRow(20, true)]
        [DataRow(17, false)]
        [DataRow(18, true)]
        [DataRow(, false)]
        public void CzyDorosly_RozneWieki_ZwracaOczekiwanaWartosc(int wiek, bool oczekiwany)
        {
            var logika = new LogikaKalkulatora();
            bool wynik = logika.CzyDorosly(wiek);

            Assert.AreEqual(oczekiwany, wynik);
        }
    }
}

Komentarz i wyjaśnienie kodu

Zauważ, że LogikaKalkulatora NIE ma ani jednej linijki związanej z WPF — żadnego Window, Button czy XAML. To WŁAŚNIE umożliwia jej przetestowanie zwykłym MSTestem, dokładnie tak samo jak klasy Sito z lekcji 2, mimo że tym razem finalna aplikacja ma pełny graficzny interfejs.

[DataTestMethod] razem z czterema atrybutami [DataRow(...)] uruchamia TEN SAM test CzyDorosly_RozneWieki_ZwracaOczekiwanaWartosc aż CZTERY razy, za każdym razem z innymi wartościami parametrów wiek i oczekiwany — Eksplorator testów pokaże je jako 4 osobne, nazwane wyniki, bez ręcznego kopiowania kodu testu cztery razy.

Ćwiczenie samodzielne

Rozdziel istniejący projekt WPF-kalkulator (z wcześniejszych lekcji działu WPF) na trzy projekty: aplikację WPF, bibliotekę Logika i projekt testowy, dokładnie jak w przykładzie. Przenieś metody obliczeniowe z code-behind/ViewModelu do klasy w bibliotece Logika i napisz dla nich testy.

Zadania do pracy własnej

  1. Dodaj do LogikaKalkulatora metody Pomnoz i napisz dla niej co najmniej 2 testy MSTest (typowy przypadek i przypadek z liczbą 0).

  2. Napisz klasę WalidacjaFormularza z metodą CzyPoprawnyEmail(string email) (prosta walidacja: zawiera "@" i przynajmniej jedną kropkę po nim) i komplet testów [DataTestMethod]/[DataRow] sprawdzających kilka poprawnych i niepoprawnych adresów naraz.

  3. Zaprojektuj i przetestuj klasę KoszykZakupowy z metodami DodajProdukt(string nazwa, decimal cena, int ilosc) i ObliczSume() — napisz testy dla pustego koszyka (suma = 0), koszyka z jednym produktem, koszyka z wieloma tymi samymi produktami (czy ilości się sumują) oraz przypadku z ujemną ceną lub ilością (powinien rzucić ArgumentException).

Typowe błędy

Umieszczanie logiki biznesowej BEZPOŚREDNIO w code-behind okna (MainWindow.xaml.cs) albo w handlerze zdarzenia przycisku — taki kod jest praktycznie NIEMOŻLIWY do przetestowania jednostkowo, bo wymagałby uruchomienia całego okna WPF. Rozwiązanie: zawsze wydzielaj logikę do osobnej klasy/biblioteki, jak w tej lekcji.

Próba "testowania" elementów UI (sprawdzanie, czy przycisk ma odpowiedni kolor, czy okno ma właściwy tytuł) zamiast logiki — to strata czasu; testy jednostkowe mają sens dla ZACHOWANIA (co program OBLICZA lub DECYDUJE), nie dla WYGLĄDU.

Zapominanie o dodaniu odwołania między projektami — projekt testowy musi mieć referencję do biblioteki Logika (nie do samego projektu WPF), inaczej kompilator nie znajdzie testowanych klas.

Nawiązanie do egzaminu zawodowego

To bezpośrednia kontynuacja MSTest z lekcji 2, zastosowana w kontekście aplikacji z pełnym interfejsem graficznym — dokładnie ten temat pojawiał się na egzaminie INF.04 w poprzednich sesjach (testy jednostkowe logiki oddzielonej od UI). W kolejnej lekcji zobaczysz, że TEN SAM zestaw testów (i ta sama technika oddzielenia Logiki od UI) działa identycznie w aplikacji mobilnej .NET MAUI — bo testy patrzą na kod w bibliotece Logika, a nie na okna czy platformę.