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.
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.
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 sprawdzenia | Co daje |
|---|---|
| Uruchomienie programu i obejrzenie wyniku | sprawdza 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 jednostkowy | wynik 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.
Jak to uruchomić
Testów nie dopisuje się do programu — trafiają do osobnego projektu, który widzi projekt testowany.
# 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.
Budowa testu: Arrange, Act, Assert
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.AreEqualto tolerancja — przy typiedoublenigdy 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
[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.
Najczęstsze asercje MSTest
| Asercja | Sprawdza |
|---|---|
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) / IsFalse | wartość 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: testowana klasa i jej testy
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";
}
}
[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));
}
}
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.
Na co uważać
| Zapis | Problem |
|---|---|
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 ekran | Nie ma czego sprawdzić. Rozdziel obliczenie od prezentacji. |
| Jeden test sprawdzający dziesięć rzeczy | Po awarii nie wiadomo, co zawiodło — pierwsza nieudana asercja przerywa test. |
| Testy zależne od siebie | Kolejność uruchamiania nie jest gwarantowana. Każdy test musi przygotować sobie dane sam (albo w [TestInitialize]). |
| Test zależny od daty albo losowania | Raz przechodzi, raz nie. Datę i generator przekazuj jako parametr, żeby dało się je ustalić. |
Zadania
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.
Zepsuj celowo testowaną metodę i zapisz, co pokazuje raport z testów: nazwę testu, wartość oczekiwaną i otrzymaną. Potem napraw metodę.
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).
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.
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].
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.
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
doublezawsze 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.