Testy jednostkowe w Visual Studio (MSTest) – WPF i .NET MAUI (INF.04)

1) Wprowadzenie: co to są testy i po co mi to na INF.04?

Wyobraź sobie prosty kalkulator. Dziś działa, ale jutro coś dopiszesz i nagle 2+2 wyjdzie 5. Testy jednostkowe to małe automatyczne programy, które sprawdzają, czy Twoje metody robią to, co trzeba.
Dzięki testom:

  • szybko widzisz błędy, zanim oddasz projekt,
  • możesz bez strachu zmieniać kod (testy krzykną, jeśli coś zepsujesz),
  • na egzaminie INF.04 pokażesz dojrzałe podejście: logika oddzielona od UI + testy logiki.

(To dokładnie ta idea, którą opisujesz już na BiteDu; tu doprecyzowuję i układam wszystko pod lekcję oraz pod MSTest w trybie offline.) bitedu.pl

2) Zasada AAA – najprościej

Każdy test składa się z trzech kroków:

  1. Arrange – przygotuj dane (np. liczby 2 i 3),
  2. Act – uruchom metodę (np. Dodaj(2,3)),
  3. Assert – sprawdź wynik (5). bitedu.pl

To wszystko. Serio.

3) Nasza przykładowa aplikacja

Zbudujemy mini-logikę kalkulatora:

  • dodawanie dwóch i trzech liczb,
  • odejmowanie dwóch i trzech liczb.

Logikę trzymamy w pliku w folderze Logika, UI (WPF lub MAUI) to tylko „skórka”. Dzięki temu testujemy czystą logikę, bez uruchamiania okien.

Struktura rozwiązania (proponowana)

KalkulatorApp.sln
│
├─ KalkulatorApp/                  ← projekt aplikacji (WPF lub MAUI)
│  ├─ Modele/
│  ├─ Walidacja/
│  ├─ Logika/
│  │   └─ Kalkulator.cs
│  ├─ MainWindow.xaml /.cs         ← WPF wariant
│  └─ MainPage.xaml /.cs           ← MAUI wariant
│
└─ KalkulatorApp.Tests/            ← projekt testowy MSTest (.NET)
   └─ KalkulatorTests.cs

4) Wdrożenie krok po kroku (OFFLINE, MSTest, Visual Studio)

Krok A. Utwórz projekt aplikacji

  • WPF: Nowy projekt → „Aplikacja WPF (.NET)”
  • .NET MAUI: Nowy projekt → „.NET MAUI App”

Nazwij projekt: KalkulatorApp.

Krok B. Dodaj folder i klasę logiki

📁 Logika/Kalkulator.cs

namespace KalkulatorApp.Logika
{
    public static class Kalkulator
    {
        // Dodawanie dwóch liczb
        public static int Dodaj2(int a, int b) => a + b;

        // Dodawanie trzech liczb
        public static int Dodaj3(int a, int b, int c) => a + b + c;

        // Odejmowanie dwóch liczb (a - b)
        public static int Odejmij2(int a, int b) => a - b;

        // Odejmowanie trzech liczb (a - b - c)
        public static int Odejmij3(int a, int b, int c) => a - b - c;
    }
}

Świadomie zero zależności od UI. Taka klasa nadaje się idealnie do testów.

Krok C. Dodaj projekt testowy MSTest (bez sieci)

  1. PPM na Rozwiązanie → Dodaj → Nowy projekt
  2. Wybierz szablon „Projekt testów MSTest (.NET)” (oficjalny szablon VS; nie wymaga internetu).
  3. Nazwij: KalkulatorApp.Tests → Utwórz.
  4. PPM na KalkulatorApp.TestsDodaj → Odwołanie… → zaznacz KalkulatorApp → OK. bitedu.pl

Krok D. Napisz testy

📄 KalkulatorApp.Tests/KalkulatorTests.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;
using KalkulatorApp.Logika;

namespace KalkulatorApp.Tests
{
    [TestClass]
    public class KalkulatorTests
    {
        // AAA: Arrange-Act-Assert w praktyce

        [TestMethod]
        public void Dodaj2_2plus3_Zwraca5()
        {
            // Arrange
            int a = 2, b = 3;

            // Act
            int wynik = Kalkulator.Dodaj2(a, b);

            // Assert
            Assert.AreEqual(5, wynik);
        }

        [TestMethod]
        public void Dodaj3_1plus2plus3_Zwraca6()
        {
            int wynik = Kalkulator.Dodaj3(1, 2, 3);
            Assert.AreEqual(6, wynik);
        }

        [TestMethod]
        public void Odejmij2_10minus4_Zwraca6()
        {
            int wynik = Kalkulator.Odejmij2(10, 4);
            Assert.AreEqual(6, wynik);
        }

        [TestMethod]
        public void Odejmij3_10minus3minus2_Zwraca5()
        {
            int wynik = Kalkulator.Odejmij3(10, 3, 2);
            Assert.AreEqual(5, wynik);
        }

        // Wzór z danymi parametrycznymi (DataRow) – opcjonalnie, jeśli szablon MSTest to wspiera
        [DataTestMethod]
        [DataRow(0, 0, 0)]
        [DataRow(-1, 1, 0)]
        [DataRow(100, 200, 300)]
        public void Dodaj2_RozneWartosci_ZwracaSume(int a, int b, int oczekiwany)
        {
            Assert.AreEqual(oczekiwany, Kalkulator.Dodaj2(a, b));
        }
    }
}

Jeśli w Twojej wersji szablonu DataTestMethod/DataRow nie jest dostępne, usuń ten test – reszta w zupełności wystarczy.

Krok E. Uruchom testy

  • Menu TestUruchom wszystkie testy.
  • Otworzy się Test Explorer z wynikiem (zielone = OK). bitedu.pl

5) Jak to „spiąć” z WPF i MAUI – dwa warianty UI

To, co poniżej, to minimalny UI do szybkiego sprawdzenia logiki „na żywo”. Testy i tak robią robotę.
Logika dalej mieszka w Logika/Kalkulator.cs – UI tylko wywołuje metody.

Wariant A: WPF

📄 MainWindow.xaml

<Window x:Class="KalkulatorApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Kalkulator (WPF)" Height="220" Width="360">
    <StackPanel Margin="16" VerticalAlignment="Center" Spacing="6">
        <TextBlock Text="a:"/>
        <TextBox x:Name="txtA" />
        <TextBlock Text="b:"/>
        <TextBox x:Name="txtB" />
        <TextBlock Text="c: (opcjonalnie do operacji na trzech)"/>
        <TextBox x:Name="txtC" />

        <WrapPanel Margin="0,8,0,0" ItemWidth="150" ItemHeight="32">
            <Button Content="Dodaj2 (a+b)" Click="Dodaj2_Click"/>
            <Button Content="Dodaj3 (a+b+c)" Click="Dodaj3_Click"/>
            <Button Content="Odejmij2 (a-b)" Click="Odejmij2_Click"/>
            <Button Content="Odejmij3 (a-b-c)" Click="Odejmij3_Click"/>
        </WrapPanel>

        <TextBlock x:Name="txtWynik" FontSize="16" FontWeight="Bold" />
    </StackPanel>
</Window>

📄 MainWindow.xaml.cs

using System;
using System.Windows;
using KalkulatorApp.Logika;

namespace KalkulatorApp
{
    public partial class MainWindow : Window
    {
        public MainWindow() => InitializeComponent();

        int GetInt(string s) => int.TryParse(s, out var v) ? v : 0;

        private void Dodaj2_Click(object sender, RoutedEventArgs e)
        {
            int a = GetInt(txtA.Text), b = GetInt(txtB.Text);
            txtWynik.Text = $"Wynik: {Kalkulator.Dodaj2(a, b)}";
        }

        private void Dodaj3_Click(object sender, RoutedEventArgs e)
        {
            int a = GetInt(txtA.Text), b = GetInt(txtB.Text), c = GetInt(txtC.Text);
            txtWynik.Text = $"Wynik: {Kalkulator.Dodaj3(a, b, c)}";
        }

        private void Odejmij2_Click(object sender, RoutedEventArgs e)
        {
            int a = GetInt(txtA.Text), b = GetInt(txtB.Text);
            txtWynik.Text = $"Wynik: {Kalkulator.Odejmij2(a, b)}";
        }

        private void Odejmij3_Click(object sender, RoutedEventArgs e)
        {
            int a = GetInt(txtA.Text), b = GetInt(txtB.Text), c = GetInt(txtC.Text);
            txtWynik.Text = $"Wynik: {Kalkulator.Odejmij3(a, b, c)}";
        }
    }
}

Wariant B: .NET MAUI

📄 MainPage.xaml

<ContentPage<br>    x:Class="KalkulatorApp.MainPage"<br>    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"<br>    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"<br>    Title="Kalkulator (MAUI)"><br><br>    <ScrollView><br>    <VerticalStackLayout Padding="20" Spacing="8"><br>        <Label Text="a:" /><br>        <Entry x:Name="entA" Keyboard="Numeric" /><br>        <Label Text="b:" /><br>        <Entry x:Name="entB" Keyboard="Numeric" /><br>        <Label Text="c: (opcjonalnie)" /><br>        <Entry x:Name="entC" Keyboard="Numeric" /><br><br>        <HorizontalStackLayout Spacing="8" Margin="0,8,0,0"><br>            <Button Text="Dodaj2" Clicked="Dodaj2_Click"/><br>            <Button Text="Dodaj3" Clicked="Dodaj3_Click"/><br>        </HorizontalStackLayout><br>        <HorizontalStackLayout Spacing="8"><br>            <Button Text="Odejmij2" Clicked="Odejmij2_Click"/><br>            <Button Text="Odejmij3" Clicked="Odejmij3_Click"/><br>        </HorizontalStackLayout><br><br>        <Label x:Name="lblWynik" FontSize="18" FontAttributes="Bold"/><br>    </VerticalStackLayout><br>    </ScrollView><br></ContentPage><br>

📄 MainPage.xaml.cs

using KalkulatorApp.Logika;

namespace KalkulatorApp
{
    public partial class MainPage : ContentPage
    {
        public MainPage() => InitializeComponent();

        int GetInt(string s) => int.TryParse(s, out var v) ? v : 0;

        void Dodaj2_Click(object sender, EventArgs e)
        {
            int a = GetInt(entA.Text), b = GetInt(entB.Text);
            lblWynik.Text = $"Wynik: {Kalkulator.Dodaj2(a, b)}";
        }

        void Dodaj3_Click(object sender, EventArgs e)
        {
            int a = GetInt(entA.Text), b = GetInt(entB.Text), c = GetInt(entC.Text);
            lblWynik.Text = $"Wynik: {Kalkulator.Dodaj3(a, b, c)}";
        }

        void Odejmij2_Click(object sender, EventArgs e)
        {
            int a = GetInt(entA.Text), b = GetInt(entB.Text);
            lblWynik.Text = $"Wynik: {Kalkulator.Odejmij2(a, b)}";
        }

        void Odejmij3_Click(object sender, EventArgs e)
        {
            int a = GetInt(entA.Text), b = GetInt(entB.Text), c = GetInt(entC.Text);
            lblWynik.Text = $"Wynik: {Kalkulator.Odejmij3(a, b, c)}";
        }
    }
}

6) Najważniejsze właściwości i atrybuty testów (MSTest)

  • [TestClass] – klasa z testami, rozpoznawana przez Test Explorer.
  • [TestMethod] – pojedynczy test.
  • [DataTestMethod] + [DataRow(...)](opcjonalnie) jeden test uruchamiany z wieloma zestawami danych.
  • Asserty (sprawdzacze):
    • Assert.AreEqual(oczekiwany, wynik) – czy równe,
    • Assert.AreNotEqual, Assert.IsTrue, Assert.IsFalse, Assert.IsNull, Assert.IsNotNull, Assert.Fail() itd. bitedu.pl

7) Podsumowanie – co zapamiętać na INF.04

  • Test jednostkowy to mały automat sprawdzający pojedyńczą metodę.
  • Trzy kroki AAA: Arrange → Act → Assert.
  • W Visual Studio od ręki utworzysz projekt MSTest i uruchomisz testy bez internetu. bitedu.pl
  • Oddziel Logika od UI (WPF/MAUI). Testuj Logikę.
  • Jeden zestaw testów działa zarówno dla WPF, jak i MAUI, bo testy patrzą na kod w Logika/, a nie na okna.

8) Zadania do samodzielnej pracy

  1. Dodaj mnożenie i dzielenie
    • W Logika/Kalkulator.cs dopisz Pomnoz2, Podziel2 (zwracaj int; jeśli dzielisz przez 0, zdecyduj: zwróć 0 albo rzuć wyjątek).
    • Napisz testy MSTest do obu metod, uwzględnij przypadki brzegowe (np. 0, liczby ujemne).
  2. Dane parametryczne
    • Przerób test Dodaj2_RozneWartosci... tak, by sprawdzał także wartości ujemne i duże liczby (więcej [DataRow]).
    • Jeśli [DataTestMethod] nie jest dostępne – napisz 3 osobne [TestMethod].
  3. Walidacja wejścia w UI
    • Dodaj prostą weryfikację do WPF/MAUI: jeśli pole puste lub nie liczba, pokaż komunikat w Label/TextBlock zamiast liczyć.
    • Logika zostaje czysta – walidację robisz w UI albo w oddzielnym helperze w folderze Walidacja.