Testy jednostkowe w MSTest

Uruchamianie programu i patrzenie na ekran sprawdza jeden przypadek. Zestaw testów sprawdza wszystkie — przy każdej zmianie kodu, w kilka sekund.

C# MSTest Assert dotnet test 60 min
CEL LEKCJI

Czego się dziś nauczysz

  • Utworzysz projekt testowy i podłączysz go do testowanego kodu
  • Napiszesz test w układzie Arrange – Act – Assert
  • Dobierzesz właściwą asercję, w tym sprawdzenie zgłoszonego wyjątku
  • Dobierzesz przypadki: zwykły, brzegowy i błędny
  • Uruchomisz testy i odczytasz raport z ich wyników

Przygotowanie: lekcje 01–55. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Po co pisać testy

W lekcji 55 sprawdzałeś kod wypisując wyniki i porównując je wzrokiem. To działa raz. Problem pojawia się przy dziesiątej poprawce: nie wiadomo, czy zmiana w jednej metodzie nie zepsuła trzech innych.

Test jednostkowy to zwykły kod, który wywołuje twoją metodę z ustalonymi danymi i sprawdza wynik. Uruchamiasz wszystkie testy jednym poleceniem i po kilku sekundach wiesz, co działa.

Rodzaj sprawdzeniaCo daje
Uruchomienie programu i obejrzenie wynikusprawdza jeden przypadek, wymaga człowieka, nie da się powtórzyć automatycznie
Console.WriteLine(wynik == oczekiwany)lepsze, ale trzeba czytać ekran i pamiętać, co ma być prawdą
Test jednostkowywynik zero-jedynkowy, powtarzalny, uruchamiany przy każdej zmianie kodu

Nazewnictwo

Jednostka to jedna metoda albo jedna klasa — testujemy ją w oderwaniu od reszty programu. Testy sprawdzające współpracę kilku części nazywa się integracyjnymi, a sprawdzające całą aplikację od strony użytkownika — funkcjonalnymi. Podstawa programowa wymienia wszystkie trzy rodzaje.

TEORIA

Jak to uruchomić

Testów nie dopisuje się do programu — trafiają do osobnego projektu, który widzi projekt testowany.

wiersz poleceń
# w folderze rozwiazania
dotnet new sln -n Turniej
dotnet new classlib -n Turniej.Logika        # kod, ktory testujemy
dotnet new mstest   -n Turniej.Testy         # projekt testowy

dotnet sln add Turniej.Logika Turniej.Testy
dotnet add Turniej.Testy reference Turniej.Logika

dotnet test                                   # uruchomienie wszystkich testow

W Visual Studio to samo robi się przez Dodaj → Nowy projekt → MSTest Test Project i Dodaj → Odwołanie do projektu. Wyniki pokazuje okno Eksplorator testów.

Testowana logika w bibliotece, nie w Main

Metody zamknięte w Main albo wypisujące wyniki na ekran testuje się fatalnie. Dlatego obliczenia trzymamy w osobnych metodach i klasach, które zwracają wynik, a wypisywanie zostawiamy w programie. To ta sama zasada, którą stosujesz od lekcji 19.

TEORIA

Budowa testu: Arrange, Act, Assert

Program.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Turniej.Logika;

namespace Turniej.Testy;

[TestClass]
public class OcenyTesty
{
    [TestMethod]
    public void Srednia_ZwyklyPrzypadek_ZwracaSrednia()
    {
        // Arrange - przygotowanie danych
        int[] oceny = { 3, 4, 5 };

        // Act - wykonanie testowanej operacji
        double wynik = Oceny.Srednia(oceny);

        // Assert - sprawdzenie wyniku
        Assert.AreEqual(4.0, wynik, 0.0001);
    }

    [TestMethod]
    public void Srednia_PustaTablica_ZglaszaWyjatek()
    {
        int[] oceny = Array.Empty<int>();

        Assert.ThrowsException<ArgumentException>(() => Oceny.Srednia(oceny));
    }
}
  • [TestClass] oznacza klasę z testami, [TestMethod] — pojedynczy test. Bez tych atrybutów metoda nie zostanie uruchomiona.
  • Nazwa testu ma opisywać przypadek: Metoda_Sytuacja_OczekiwanyWynik. Gdy test zaświeci się na czerwono, sama nazwa mówi, co jest nie tak.
  • Układ Arrange – Act – Assert trzyma test w ryzach: przygotuj, wykonaj, sprawdź. Jeden test sprawdza jedną rzecz.
  • Trzeci argument Assert.AreEqual to tolerancja — przy typie double nigdy nie porównujemy dokładnie (lekcja 10).
  • () => Oceny.Srednia(oceny) to lambda z lekcji 53: przekazujemy czynność, którą test ma wykonać i sprawdzić, czy zgłosi wyjątek.

Jeden test, wiele zestawów danych

Program.cs
[TestMethod]
[DataRow(0, "niedostateczny")]
[DataRow(50, "dostateczny")]
[DataRow(90, "bardzo dobry")]
[DataRow(100, "bardzo dobry")]
public void Slownie_RozneProgi_ZwracaNazwe(int punkty, string oczekiwane)
{
    Assert.AreEqual(oczekiwane, Oceny.Slownie(punkty));
}

Każdy wiersz [DataRow] to osobne uruchomienie testu. Cztery przypadki brzegowe w ośmiu wierszach kodu.

ELEMENTY WBUDOWANE

Najczęstsze asercje MSTest

AsercjaSprawdza
Assert.AreEqual(ocz, wynik)równość wartości. Dla double podaj trzeci argument — tolerancję.
Assert.AreNotEqual(a, b)że wartości są różne
Assert.IsTrue(warunek) / IsFalsewartość logiczną. Dodaj komunikat jako drugi argument, bo sam false niewiele mówi.
Assert.IsNull(x) / IsNotNull(x)obecność albo brak wartości
Assert.AreSame(a, b)czy to ten sam obiekt (tożsamość, nie zawartość)
Assert.ThrowsException<T>(() => …)czy operacja zgłasza wyjątek podanego typu
CollectionAssert.AreEqual(a, b)równość zawartości dwóch kolekcji, element po elemencie
[TestInitialize]metoda uruchamiana przed każdym testem — do przygotowania danych
[TestCleanup]metoda uruchamiana po każdym teście — do sprzątania
[DataRow(...)]kolejny zestaw danych dla tego samego testu

Najpierw zobacz, że test potrafi się nie udać

Po napisaniu testu zepsuj celowo testowaną metodę i sprawdź, czy test zaświecił się na czerwono. Test, który przechodzi zawsze — również dla błędnego kodu — jest gorszy niż brak testu, bo daje fałszywe poczucie bezpieczeństwa.

PRZYKŁAD

Przykład: testowana klasa i jej testy

Turniej.Logika/Oceny.cs
namespace Turniej.Logika;

public static class Oceny
{
    public static double Srednia(int[] oceny)
    {
        if (oceny == null || oceny.Length == 0)
        {
            throw new ArgumentException("Brak ocen.", nameof(oceny));
        }

        long suma = 0;
        foreach (int o in oceny) suma += o;

        return (double)suma / oceny.Length;
    }

    public static string Slownie(int punkty)
    {
        if (punkty < 0 || punkty > 100)
        {
            throw new ArgumentOutOfRangeException(nameof(punkty));
        }

        if (punkty >= 90) return "bardzo dobry";
        if (punkty >= 50) return "dostateczny";
        return "niedostateczny";
    }
}
Turniej.Testy/OcenyTesty.cs
[TestClass]
public class OcenyTesty
{
    [TestMethod]
    public void Srednia_TrzyOceny_ZwracaCztery()
    {
        Assert.AreEqual(4.0, Oceny.Srednia(new[] { 3, 4, 5 }), 0.0001);
    }

    [TestMethod]
    public void Srednia_JednaOcena_ZwracaTaSamaWartosc()
    {
        Assert.AreEqual(5.0, Oceny.Srednia(new[] { 5 }), 0.0001);
    }

    [TestMethod]
    public void Srednia_NieCalkowita_NieUcinaCzesciUlamkowej()
    {
        Assert.AreEqual(3.5, Oceny.Srednia(new[] { 3, 4 }), 0.0001);
    }

    [TestMethod]
    public void Srednia_PustaTablica_ZglaszaArgumentException()
    {
        Assert.ThrowsException<ArgumentException>(
            () => Oceny.Srednia(Array.Empty<int>()));
    }

    [TestMethod]
    [DataRow(-1)]
    [DataRow(101)]
    public void Slownie_PozaZakresem_ZglaszaWyjatek(int punkty)
    {
        Assert.ThrowsException<ArgumentOutOfRangeException>(
            () => Oceny.Slownie(punkty));
    }
}
dotnet test
Passed!  - Failed:     0, Passed:     6, Skipped:     0, Total:     6

Co dzieje się po kolei

  • Test trzeci pilnuje najczęstszego błędu z tego kursu: dzielenia całkowitego. Usuń rzutowanie (double), a test wskaże dokładnie tę metodę.
  • Testy obejmują przypadek zwykły, brzegowy (jedna ocena) i błędny (pusta tablica) — to minimalny zestaw dla każdej metody.
  • Dwa [DataRow] dają dwa uruchomienia jednego testu, stąd sześć wyników przy pięciu metodach.
  • Testy nic nie wypisują. Wynikiem jest zielony albo czerwony status, a nie tekst do przeczytania.
CZĘSTE BŁĘDY

Na co uważać

ZapisProblem
Test bez [TestMethod]Nie zostanie uruchomiony. Zielony wynik nic wtedy nie znaczy.
Assert.AreEqual(0.3, 0.1 + 0.2)Porównanie double bez tolerancji. Podaj trzeci argument.
Kolejność argumentów AreEqual(wynik, oczekiwane)Test zadziała, ale komunikat o błędzie zamieni miejscami wartość oczekiwaną i rzeczywistą. Oczekiwana jest pierwsza.
Testowanie metody, która wypisuje na ekranNie ma czego sprawdzić. Rozdziel obliczenie od prezentacji.
Jeden test sprawdzający dziesięć rzeczyPo awarii nie wiadomo, co zawiodło — pierwsza nieudana asercja przerywa test.
Testy zależne od siebieKolejność uruchamiania nie jest gwarantowana. Każdy test musi przygotować sobie dane sam (albo w [TestInitialize]).
Test zależny od daty albo losowaniaRaz przechodzi, raz nie. Datę i generator przekazuj jako parametr, żeby dało się je ustalić.
ZADANIA

Zadania

ZAD 1Pierwszy projekt testowy★☆☆

Utwórz rozwiązanie z biblioteką i projektem MSTest, dodaj odwołanie i napisz jeden test sprawdzający metodę Pole(4, 7) == 28. Uruchom dotnet test.

ZAD 2Czerwony, potem zielony★☆☆

Zepsuj celowo testowaną metodę i zapisz, co pokazuje raport z testów: nazwę testu, wartość oczekiwaną i otrzymaną. Potem napraw metodę.

ZAD 3Trzy przypadki★★☆

Napisz metodę public static bool CzyOcena(int wartosc), która zwraca true dla liczb od 1 do 6 włącznie, a false dla pozostałych. Następnie napisz do niej testy: przypadek typowy (4), obie wartości brzegowe (1 i 6) oraz dwie niepoprawne (0 i 7).

ZAD 4DataRow★★☆

Napisz jeden test metody CzyOcena obsługujący pięć zestawów danych przy pomocy atrybutów [DataRow]: (1, true), (4, true), (6, true), (0, false) i (7, false). Uruchom testy i sprawdź, ile wyników pokazuje raport dla tej jednej metody.

ZAD 5Testy klasy Magazyn★★☆

Napisz klasę Magazyn z prywatnym polem stan ustawianym w konstruktorze, właściwością Stan tylko do odczytu i metodą bool Pobierz(int ile). Metoda zmniejsza stan i zwraca true, gdy towaru wystarcza; w przeciwnym razie zwraca false i zostawia stan bez zmian. Wartość ujemna ma być odrzucona.

Do tej klasy napisz pięć testów: stan początkowy, poprawne pobranie, pobranie większe niż stan, pobranie zera i pobranie wartości ujemnej. Świeży magazyn przed każdym testem utwórz w metodzie oznaczonej [TestInitialize].

ZAD 6Testy sortowania★★★

Napisz własną metodę public static void Sortuj(int[] tablica) — dowolnym algorytmem — a następnie zestaw testów sprawdzających ją w sześciu sytuacjach: tablica pusta, jednoelementowa, już posortowana, posortowana odwrotnie, z powtórzeniami oraz zawierająca int.MinValue i int.MaxValue.

W każdym teście porównaj wynik z tablicą posortowaną przez Array.Sort, używając CollectionAssert.AreEqual. Pamiętaj, żeby obie tablice miały te same dane wejściowe — skorzystaj z Clone.

PODSUMOWANIE

Co trzeba zapamiętać

  • Test jednostkowy to kod sprawdzający jedną metodę na ustalonych danych — powtarzalnie i bez udziału człowieka.
  • Testy trafiają do osobnego projektu z odwołaniem do projektu testowanego.
  • [TestClass] i [TestMethod] są obowiązkowe; nazwa testu opisuje przypadek.
  • Układ Arrange – Act – Assert: przygotuj dane, wykonaj operację, sprawdź wynik.
  • Dla double zawsze podawaj tolerancję; wartość oczekiwana jest pierwszym argumentem.
  • Minimalny zestaw dla metody: przypadek zwykły, brzegowy i błędny. Sprawdź też, że test potrafi się nie udać.

Dokumentacja: Microsoft Learn — temat tej lekcji.