Testy jednostkowe w C# – MSTest
Od podstaw testowania, przez asercje i wzorzec AAA, aż po testy parametryzowane i testowanie wyjątków.
Wstęp – po co testy jednostkowe?
Testy jednostkowe (ang. Unit Tests) to małe fragmenty kodu, które automatycznie sprawdzają, czy pojedyncza funkcja/metoda działa poprawnie.
Dlaczego są ważne?
| Powód | Dlaczego to jest ważne |
|---|---|
| Szybkie wykrywanie błędów | Od razu wiemy, co nie działa – nie trzeba ręcznie klikać przez całą aplikację |
| Pewność poprawności | Testy potwierdzają, że wynik jest poprawny po każdej zmianie |
| Łatwiejsza rozbudowa | Bez lęku dodajemy nowe funkcje – testy powiedzą, czy coś zepsujemy |
| Profesjonalne podejście | Tak pracuje każda poważna firma IT |
| Dokumentacja kodu | Testy pokazują, jak używać danej funkcji |
Przykład z życia – kalkulator
❌ Bez testów:
✅ Z testami:
Co to jest MSTest?
MSTest to framework wbudowany w Visual Studio, który służy do pisania testów jednostkowych w C#. Microsoft go stworzył specjalnie dla .NET.
Atrybuty MSTest
W testach używamy specjalnych atrybutów – to adnotacje przed klasą lub metodą:
| Atrybut | Znaczenie | Przykład |
|---|---|---|
[TestClass] | Oznacza klasę zawierającą testy | [TestClass] public class CalculatorTests |
[TestMethod] | Oznacza pojedynczy test | [TestMethod] public void Test_Dodawania() |
[DataTestMethod] | Test parametryzowany | [DataTestMethod] z [DataRow] |
[TestInitialize] | Metoda wykonywana przed każdym testem | Przygotowanie danych testowych |
[TestCleanup] | Metoda wykonywana po każdym teście | Czyszczenie zasobów |
Konfiguracja projektu testowego
Krok 1: Utwórz projekt główny
- Otwórz Visual Studio
- File → New → Project
- Wybierz
Console App (.NET) - Nazwa:
CalculatorApp - Kliknij Create
Krok 2: Dodaj projekt testowy
- Kliknij prawym przyciskiem na Solution (w Solution Explorer)
- Add → New Project
- Wyszukaj:
MSTest Test Project - Nazwa:
CalculatorApp.Tests - Kliknij Create
Krok 3: Dodaj referencję do projektu głównego
Projekt testowy musi „widzieć” projekt główny. Bez tego testy nie znajdą klas do testowania!
- W Solution Explorer, rozwiń projekt
CalculatorApp.Tests - Kliknij prawym na Dependencies → Add Project Reference
- Zaznacz
CalculatorApp - Kliknij OK
Krok 4: Struktura projektu
CalculatorApp← projekt główny
Program.cs
Calculator.cs
CalculatorApp.Tests← projekt testowy
Dependencies
CalculatorApp← musi tu być!
CalculatorTests.cs
Krok 5: Sprawdź, czy działa
W pliku CalculatorTests.cs dodaj na górze:
using CalculatorApp; // Jeśli się podkreśla na czerwono = brak referencji!
Test Explorer – centrum dowodzenia
Test Explorer to okno w Visual Studio, gdzie widzisz wszystkie testy i możesz je uruchamiać.
Jak otworzyć Test Explorer?
- Metoda 1:
Test → Test Explorer - Metoda 2: Ctrl + E, T
Co zobaczysz w Test Explorer?
✅ Test passed (zielony)
Test zakończony sukcesem – wszystko działa!
❌ Test failed (czerwony)
Test wykrył błąd – coś nie działa!
Test nie został jeszcze uruchomiony. Kliknij „Run All” aby uruchomić wszystkie testy.
Co to jest asercja?
Asercja (ang. assertion) to instrukcja sprawdzająca, czy wynik działania programu jest taki, jak oczekiwany.
Assert.AreEqual(5, wynik); // ✅ Test przechodzi, jeśli wynik == 5 // ❌ Test nie przechodzi, jeśli wynik != 5
Najważniejsze asercje
| Asercja | Znaczenie | Przykład |
|---|---|---|
Assert.AreEqual(expected, actual) | Sprawdza równość | Assert.AreEqual(5, suma) |
Assert.AreNotEqual(expected, actual) | Sprawdza nierówność | Assert.AreNotEqual(0, wynik) |
Assert.IsTrue(condition) | Warunek musi być true | Assert.IsTrue(liczba > 0) |
Assert.IsFalse(condition) | Warunek musi być false | Assert.IsFalse(lista.IsEmpty) |
Assert.IsNull(object) | Obiekt musi być null | Assert.IsNull(pustyObiekt) |
Assert.IsNotNull(object) | Obiekt nie może być null | Assert.IsNotNull(uzytkownik) |
Assert.ThrowsException<T>() | Sprawdza, czy rzucono wyjątek | Assert.ThrowsException<DivideByZeroException>() |
Możesz dodać własny komunikat, który pojawi się gdy test nie przejdzie:
Assert.AreEqual(5, wynik, "Suma 2+3 powinna być równa 5!");
Kod programu do testowania
Stwórzmy prostą klasę Calculator w projekcie głównym.
namespace CalculatorApp { public class Calculator { public static int Dodaj(int a, int b) { return a + b; } public static int Odejmij(int a, int b) { return a - b; } public static int Pomnoz(int a, int b) { return a * b; } public static double Podziel(int a, int b) { if (b == 0) { throw new DivideByZeroException("Nie można dzielić przez zero!"); } return (double)a / b; } public static string GenerujKomunikat(int a, int b, int wynik) { return $"Suma {a} + {b} = {wynik}"; } } }
Pierwszy test – wzorzec AAA
Każdy dobry test składa się z trzech sekcji: Arrange → Act → Assert.
using Microsoft.VisualStudio.TestTools.UnitTesting; using CalculatorApp; namespace CalculatorApp.Tests { [TestClass] public class CalculatorTests { [TestMethod] public void Dodaj_DwieLiczbyDodatnie_ZwracaSume() { // Arrange (przygotowanie danych) int a = 2; int b = 3; int oczekiwanyWynik = 5; // Act (wykonanie metody) int wynik = Calculator.Dodaj(a, b); // Assert (sprawdzenie wyniku) Assert.AreEqual(oczekiwanyWynik, wynik); } } }
Wzorzec AAA sprawia, że testy są czytelne i zrozumiałe dla każdego. Od razu widać: co przygotowujemy, co testujemy i czego oczekujemy.
Uruchamianie testów
- Otwórz Test Explorer (Ctrl + E, T)
- Kliknij Run All (zielona strzałka ▶️)
- Poczekaj 1-2 sekundy
Debugowanie testów
Jeśli test nie przechodzi i nie wiesz dlaczego:
- Postaw breakpoint (F9) w linii z asercją
- Kliknij prawym na test → Debug
- Program zatrzyma się na breakpoincie – możesz podejrzeć wartości zmiennych
Różne przypadki testowe
Dobry programista testuje wiele scenariuszy, nie tylko „happy path” (idealny przypadek).
Test z liczbami ujemnymi
[TestMethod] public void Dodaj_DwieLiczbyUjemne_ZwracaSume() { // Arrange int a = -5; int b = -3; // Act int wynik = Calculator.Dodaj(a, b); // Assert Assert.AreEqual(-8, wynik); }
Test z zerem
[TestMethod] public void Dodaj_JednaLiczbaZero_ZwracaDrugaLiczbe() { // Arrange int a = 0; int b = 7; // Act int wynik = Calculator.Dodaj(a, b); // Assert Assert.AreEqual(7, wynik); }
Test mnożenia
[TestMethod] public void Pomnoz_DwieLiczby_ZwracaIloczyn() { // Arrange int a = 4; int b = 5; // Act int wynik = Calculator.Pomnoz(a, b); // Assert Assert.AreEqual(20, wynik); }
Testowanie wyjątków
Czasami oczekujemy, że kod RZUCI WYJĄTEK (np. dzielenie przez zero). MSTest pozwala to przetestować!
Przykład: dzielenie przez zero
[TestMethod] public void Podziel_PrzezZero_RzucaWyjatek() { // Arrange int a = 10; int b = 0; // Act & Assert (w jednej linii!) Assert.ThrowsException<DivideByZeroException>(() => { Calculator.Podziel(a, b); }); }
() => { } to lambda (funkcja anonimowa).
MSTest uruchomi ten kod i sprawdzi, czy rzucił DivideByZeroException.
✅ Jeśli rzuci – test przechodzi
❌ Jeśli NIE rzuci – test nie przechodzi
Test dzielenia z resztą (delta)
[TestMethod] public void Podziel_ZReszta_ZwracaWartoscZmiennoprzecinkowa() { // Arrange int a = 10; int b = 3; // Act double wynik = Calculator.Podziel(a, b); // Assert - delta = dopuszczalny błąd zaokrąglenia Assert.AreEqual(3.333, wynik, 0.001); }
Przy liczbach zmiennoprzecinkowych używaj trzeciego parametru (delta), bo może być błąd zaokrąglenia! 0.001 oznacza tolerancję ±0.001.
Testy parametryzowane (DataRow)
Zamiast pisać wiele podobnych testów, możemy użyć jednego testu z wieloma zestawami danych.
❌ Bez parametryzacji
Dużo powtarzającego się kodu:
Test1(), Test2(), Test3()…
✅ Z parametryzacją
Jeden test, wiele przypadków:
[DataRow(2,3,5)], [DataRow(7,3,10)]…
[DataTestMethod] [DataRow(2, 3, 5)] [DataRow(7, 3, 10)] [DataRow(-5, 5, 0)] [DataRow(0, 0, 0)] [DataRow(100, 200, 300)] public void Dodaj_RozneWartosci_ZwracaPoprawnaSume(int a, int b, int oczekiwany) { // Act int wynik = Calculator.Dodaj(a, b); // Assert Assert.AreEqual(oczekiwany, wynik); }
Jak to działa?
[DataTestMethod]– zamiast[TestMethod][DataRow(a, b, oczekiwany)]– każdy wiersz to jeden test- MSTest uruchomi metodę 5 razy (dla każdego DataRow)
- W Test Explorer zobaczysz 5 osobnych testów!
✅ Mniej kodu
✅ Łatwo dodać nowe przypadki testowe
✅ Wszystkie testy widoczne w Test Explorer osobno
Testowanie komunikatów tekstowych
Często w programie wyświetlamy komunikaty użytkownikowi. Możemy to też testować!
❌ Console.WriteLine()
Nie da się testować – testy nie mają dostępu do konsoli
✅ return string
Zwracaj tekst zamiast wypisywać – można testować!
[TestMethod] public void GenerujKomunikat_DwieLiczby_ZwracaPoprawnyTekst() { // Arrange int a = 5; int b = 3; int wynik = 8; // Act string komunikat = Calculator.GenerujKomunikat(a, b, wynik); // Assert Assert.AreEqual("Suma 5 + 3 = 8", komunikat); }
Test komunikatu z DataRow
[DataTestMethod] [DataRow(2, 3, 5, "Suma 2 + 3 = 5")] [DataRow(0, 0, 0, "Suma 0 + 0 = 0")] [DataRow(-1, 1, 0, "Suma -1 + 1 = 0")] public void GenerujKomunikat_RozneWartosci(int a, int b, int wynik, string oczekiwany) { // Act string komunikat = Calculator.GenerujKomunikat(a, b, wynik); // Assert Assert.AreEqual(oczekiwany, komunikat); }
Częste błędy
❌ Błąd 1: Brak atrybutów [TestClass] lub [TestMethod]
Test Explorer nie widzi testów, bo klasa lub metoda nie jest oznaczona.
❌ Źle
public class CalculatorTests
public void TestDodawania()
→ Test niewidoczny!
✅ Dobrze
[TestClass]
public class CalculatorTests
[TestMethod]
public void TestDodawania()
❌ Błąd 2: Brak referencji do projektu głównego
Visual Studio podkreśla using CalculatorApp; na czerwono.
Dependencies → Add Project Reference → Zaznacz projekt główny
❌ Błąd 3: Zła kolejność argumentów w AreEqual
❌ Źle
Assert.AreEqual(wynik, 5);
Komunikat: Expected:<6>. Actual:<5>
(mylące!)
✅ Dobrze
Assert.AreEqual(5, wynik);
Komunikat: Expected:<5>. Actual:<6>
(jasne!)
❌ Błąd 4: [TestMethod] zamiast [DataTestMethod]
Przy testach parametryzowanych z [DataRow] musisz użyć [DataTestMethod]!
❌ Błąd 5: Porównywanie double bez delta
❌ Źle
Assert.AreEqual(3.333333, wynik);
Może nie przejść przez błąd zaokrąglenia!
✅ Dobrze
Assert.AreEqual(3.333, wynik, 0.001);
Delta = tolerancja błędu
❌ Błąd 6: Testy zależne od siebie
Jeden test ustawia zmienną, a drugi z niej korzysta. Kolejność wykonania testów nie jest gwarantowana!
Każdy test musi działać niezależnie. Nie polegaj na kolejności wykonywania testów!
Dobre praktyki
1. Konwencja nazewnictwa testów
Wzorzec: NazwaMetody_Scenariusz_OczekiwaneZachowanie
✅ Dobre nazwy
Dodaj_DwieLiczbyDodatnie_ZwracaSume
Podziel_PrzezZero_RzucaWyjatek
ZalogujUzytkownika_NiepoprawneHaslo_ZwracaFalse
❌ Złe nazwy
Test1, Test2 – nic nie mówią
TestDodawania – zbyt ogólne
DodajTest – niejasne
2. Jeden assert na test (w miarę możliwości)
Jeśli pierwszy assert nie przejdzie, pozostałe się nie wykonają – nie dowiesz się, co jeszcze jest zepsute.
3. Testuj przypadki brzegowe
Przypadki brzegowe (ang. edge cases) to wartości na „krawędziach” dozwolonego zakresu lub sytuacje nietypowe, które często powodują błędy.
| Typ przypadku | Przykład | Dlaczego ważne? |
|---|---|---|
| Wartości brzegowe | 0, -1, int.MaxValue | Granice zakresu często powodują błędy |
| Wartości null | Przekazanie null | Najczęstszy wyjątek to NullReferenceException |
| Puste kolekcje | Lista z 0 elementów, pusty string | Pętle mogą się zachować nieprzewidywalnie |
| Jeden element | Lista z 1 elementem | Algorytmy sortowania mogą mieć błędy |
| Znaki specjalne | Spacje, emotki, polskie znaki | Problemy z kodowaniem |
Dla metody CzyWiekPoprawny(int wiek) z zakresem 0-150:
✅ Wewnątrz zakresu: 25, 50, 100 → powinny przejść
✅ Na krawędziach: 0 i 150 → powinny przejść
❌ Tuż za krawędziami: -1 i 151 → powinny NIE przejść
❓ Ekstremalne: -1000, int.MaxValue → jak się zachowa?
4. Testy muszą być niezależne
Każdy test powinien działać samodzielnie. Nie polegaj na kolejności wykonywania!
5. Testy powinny być szybkie
⚡ Jeden test powinien wykonywać się w milisekundach, nie sekundach!
• Operacji na plikach (jeśli nie konieczne)
• Połączeń z bazą danych
• Żądań HTTP do internetu
• Thread.Sleep()
6. Używaj [TestInitialize]
Jeśli wiele testów potrzebuje tych samych danych:
[TestClass] public class CalculatorTests { private Calculator kalkulator; [TestInitialize] public void Setup() { // Wykonywane przed KAŻDYM testem kalkulator = new Calculator(); } [TestMethod] public void Test1() { /* użyj kalkulator */ } [TestMethod] public void Test2() { /* użyj kalkulator */ } }
Zadania praktyczne
📝 Zadanie 1: Podstawowe testy
Dodaj do klasy Calculator metodę:
public static int Poteguj(int podstawa, int wykladnik) { return (int)Math.Pow(podstawa, wykladnik); }
Napisz 3 testy: 2³ = 8, 5² = 25, 10⁰ = 1
📝 Zadanie 2: Testowanie warunków
Dodaj metodę:
public static bool CzyParzysta(int liczba) { return liczba % 2 == 0; }
Napisz testy sprawdzające:
- Liczba parzysta (np. 4) →
true - Liczba nieparzysta (np. 7) →
false - Zero →
true - Liczba ujemna parzysta (np. -6) →
true
💡 Podpowiedź: Użyj Assert.IsTrue() i Assert.IsFalse()
📝 Zadanie 3: Test parametryzowany
Stwórz JEDEN test parametryzowany dla metody Pomnoz, który sprawdzi:
- 3 × 4 = 12
- 5 × 5 = 25
- 0 × 10 = 0
- -2 × 3 = -6
💡 Podpowiedź: Użyj [DataTestMethod] i [DataRow]
📝 Zadanie 4: Testowanie wyjątków
Dodaj metodę:
public static int Pierwiastek(int liczba) { if (liczba < 0) { throw new ArgumentException("Nie można obliczyć pierwiastka z liczby ujemnej!"); } return (int)Math.Sqrt(liczba); }
Napisz 2 testy:
- Pierwiastek z 16 = 4 (test normalny)
- Pierwiastek z -5 → rzuca
ArgumentException(test wyjątku)
📝 Zadanie 5: Walidacja danych
Dodaj metodę:
public static bool CzyWiekPoprawny(int wiek) { return wiek >= 0 && wiek <= 150; }
Napisz 5 testów (przypadki brzegowe!):
- Wiek 25 →
true - Wiek 0 →
true(brzeg) - Wiek 150 →
true(brzeg) - Wiek -1 →
false - Wiek 151 →
false
⭐ Zadanie 6: Projekt końcowy
Stwórz klasę StringHelper z metodami:
public class StringHelper { public static string Odwroc(string tekst) { /* TODO */ } public static int IleLiter(string tekst) { /* TODO */ } public static bool CzyPalindrom(string tekst) { /* TODO */ } }
Napisz minimum 10 testów sprawdzających wszystkie metody w różnych scenariuszach (w tym przypadki brzegowe: pusty string, null, jeden znak).
⭐ Bonus: Użyj testów parametryzowanych dla CzyPalindrom