1. Wstęp – po co testy jednostkowe?
Testy jednostkowe (ang. Unit Tests) to małe fragmenty kodu, które sprawdzają, czy pojedyncza funkcja/metoda działa poprawnie. Ich zadaniem jest automatycznie wykrywać błędy i upewniać się, że program zachowuje się zgodnie z oczekiwaniami.
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 działania | Testy potwierdzają, że wynik jest poprawny po każdej zmianie |
| Łatwiejsza rozbudowa aplikacji | Bez lęku dodajemy nowe funkcje – testy powiedzą nam, 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
Wyobraź sobie, że tworzysz kalkulator. Bez testów:
- ✍️ Piszesz funkcję dodawania
- 🖱️ Uruchamiasz program
- ⌨️ Wpisujesz liczby ręcznie
- 👀 Sprawdzasz wynik
- 🔁 Powtarzasz to za każdym razem, gdy coś zmienisz
Z testami:
- ✍️ Piszesz funkcję dodawania
- 🧪 Piszesz test, który automatycznie sprawdza 10 różnych przypadków
- ⚡ Klikasz jeden przycisk – wszystkie testy wykonują się w sekundę
- ✅ Od razu widzisz, czy wszystko działa
2. Co to jest MSTest?
MSTest to framework (narzędzie/biblioteka programistyczna) wbudowany w Visual Studio, który służy do pisania testów jednostkowych w języku C#. Microsoft go stworzył specjalnie dla .NET.
Atrybuty MSTest
W testach używamy specjalnych atrybutów (ang. attributes) – 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 |
3. Instalacja i konfiguracja projektu testowego
Krok 1: Utwórz projekt główny
- Otwórz Visual Studio
- File → New → Project
- Wybierz Console App (.NET) lub Console App (.NET Framework)
- 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
To najważniejszy krok! Projekt testowy musi „widzieć” projekt główny.
- W Solution Explorer, rozwiń projekt
CalculatorApp.Tests - Kliknij prawym na Dependencies → Add Project Reference
- Zaznacz
CalculatorApp - Kliknij OK
Krok 4: Struktura projektu
Po poprawnej konfiguracji powinieneś mieć:
Solution 'CalculatorApp'
├── CalculatorApp (projekt główny)
│ ├── Program.cs
│ └── Calculator.cs
└── CalculatorApp.Tests (projekt testowy)
├── Dependencies
│ └── Projects
│ └── CalculatorApp ← musi tu być!
└── CalculatorTests.csKrok 5: Sprawdź, czy działa
W pliku CalculatorTests.cs dodaj na górze:
using CalculatorApp; // Jeśli się podkreśla na czerwono = brak referencji!4. Test Explorer – centrum dowodzenia testami
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
❌ Test failed (czerwony) – test wykrył błąd
⚠️ Test not run (szary) – test nie został jeszcze uruchomiony5. Co to jest asercja?
Asercja (ang. assertion) to instrukcja sprawdzająca, czy wynik działania programu jest taki, jak oczekiwany.
Podstawowy przykład
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ć prawdziwy | Assert.IsTrue(liczba > 0) |
Assert.IsFalse(condition) | Warunek musi być fałszywy | Assert.IsFalse(tekst.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>() |
Komunikaty w asercjach (opcjonalne)
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!");6. Kod programu do testowania
Stwórzmy prostą klasę Calculator w projekcie głównym.
Plik: Calculator.cs
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}";
}
}
}Ta klasa reprezentuje prosty kalkulator z podstawowymi operacjami matematycznymi.
7. Pierwszy test jednostkowy
Plik: CalculatorTests.cs
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 (Arrange-Act-Assert)
Każdy dobry test składa się z trzech sekcji:
| Etap | Znaczenie | Gdzie w kodzie | Co robimy |
|---|---|---|---|
| Arrange | Przygotowanie danych | int a = 2; int b = 3; | Tworzymy zmienne testowe |
| Act | Wykonanie testowanej metody | Calculator.Dodaj(a, b) | Wywołujemy badaną funkcję |
| Assert | Sprawdzenie wyniku | Assert.AreEqual(5, wynik) | Sprawdzamy, czy wynik jest OK |
💡 Wskazówka: Używanie tego wzorca sprawia, że testy są czytelne i zrozumiałe!
8. Uruchamianie testów i interpretacja wyników
Uruchomienie testu
- Otwórz Test Explorer (
Ctrl + E, T) - Kliknij Run All (zielona strzałka ▶️)
- Poczekaj 1-2 sekundy
Możliwe wyniki
✅ Test zakończony sukcesem (zielony)
✅ Dodaj_DwieLiczbyDodatnie_ZwracaSume
Czas: 23msZnaczenie: Wszystko działa poprawnie! 🎉
❌ Test nie przeszedł (czerwony)
❌ Dodaj_DwieLiczbyDodatnie_ZwracaSume
Assert.AreEqual failed. Expected:<5>. Actual:<6>.
Czas: 18msZnaczenie: Coś jest źle! Spodziewaliśmy się 5, a dostaliśmy 6.
Przykład nieudanego testu
[TestMethod]
public void Dodaj_TestKtoryNiePrzejdzie()
{
int wynik = Calculator.Dodaj(2, 3);
Assert.AreEqual(10, wynik); // ❌ Spodziewamy się 10, ale będzie 5!
}Rezultat w Test Explorer:
❌ Dodaj_TestKtoryNiePrzejdzie
Assert.AreEqual failed. Expected:<10>. Actual:<5>.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
9. 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 z dużymi liczbami
[TestMethod]
public void Dodaj_DuzeLiczby_ZwracaSume()
{
// Arrange
int a = 1000000;
int b = 2000000;
// Act
int wynik = Calculator.Dodaj(a, b);
// Assert
Assert.AreEqual(3000000, wynik);
}Test odejmowania
[TestMethod]
public void Odejmij_DwieLiczby_ZwracaRoznice()
{
// Arrange
int a = 10;
int b = 3;
// Act
int wynik = Calculator.Odejmij(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);
}10. 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);
});
}Wyjaśnienie składni
Assert.ThrowsException<DivideByZeroException>(() =>
{
// Kod, który POWINIEN rzucić wyjątek
});() => { }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 poprawnego dzielenia
[TestMethod]
public void Podziel_DwieLiczby_ZwracaIloraz()
{
// Arrange
int a = 10;
int b = 2;
// Act
double wynik = Calculator.Podziel(a, b);
// Assert
Assert.AreEqual(5.0, wynik);
}Test dzielenia z resztą
[TestMethod]
public void Podziel_ZReszta_ZwracaWartoscZmiennoprzecinkowa()
{
// Arrange
int a = 10;
int b = 3;
// Act
double wynik = Calculator.Podziel(a, b);
// Assert
Assert.AreEqual(3.333, wynik, 0.001); // Delta = dopuszczalny błąd
}💡 Wskazówka: Przy liczbach zmiennoprzecinkowych używaj trzeciego parametru (delta), bo może być błąd zaokrąglenia!
Bez parametryzacji (powtórzenia)
[TestMethod]
public void Dodaj_Test1() { Assert.AreEqual(5, Calculator.Dodaj(2, 3)); }
[TestMethod]
public void Dodaj_Test2() { Assert.AreEqual(10, Calculator.Dodaj(7, 3)); }
[TestMethod]
public void Dodaj_Test3() { Assert.AreEqual(0, Calculator.Dodaj(-5, 5)); }😩 Problem: Dużo powtarzającego się kodu!
Z parametryzacją (elegancko!)
[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!
Wynik w Test Explorer
✅ Dodaj_RozneWartosci_ZwracaPoprawnaSume (2, 3, 5)
✅ Dodaj_RozneWartosci_ZwracaPoprawnaSume (7, 3, 10)
✅ Dodaj_RozneWartosci_ZwracaPoprawnaSume (-5, 5, 0)
✅ Dodaj_RozneWartosci_ZwracaPoprawnaSume (0, 0, 0)
✅ Dodaj_RozneWartosci_ZwracaPoprawnaSume (100, 200, 300)Zalety
✅ Mniej kodu
✅ Łatwo dodać nowe przypadki testowe
✅ Wszystkie testy widoczne w Test Explorer
12. Testowanie komunikatów tekstowych
Często w programie wyświetlamy komunikaty użytkownikowi. Możemy to też testować!
❌ Problem: nie można testować Console.WriteLine()
// ❌ To NIE ZADZIAŁA w testach!
public static void WypiszSume(int a, int b)
{
Console.WriteLine($"Suma {a} + {b} = {a + b}");
}Dlaczego? Bo testy nie mają dostępu do konsoli. Console.WriteLine() tylko wypisuje tekst, którego nie możemy przechwycić.
✅ Rozwiązanie: zwracaj string zamiast wypisywać
public static string GenerujKomunikat(int a, int b, int wynik)
{
return $"Suma {a} + {b} = {wynik}";
}Teraz możemy to przetestować!
Test komunikatu
[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);
}13. Dobre praktyki
1. Konwencja nazewnictwa testów
Wzorzec: NazwaMetody_Scenariusz_OczekiwaneZachowanie
✅ Dobre przykłady:
Dodaj_DwieLiczbyDodatnie_ZwracaSumePodziel_PrzezZero_RzucaWyjatekZalogujUzytkownika_NiepoprawneHaslo_ZwracaFalse
❌ Złe przykłady:
Test1,Test2– nic nie mówiąTestDodawania– zbyt ogólneDodajTest– niejasne
2. Jeden assert na test (w miarę możliwości)
✅ Dobrze:
[TestMethod]
public void Dodaj_DwieLiczby_ZwracaSume()
{
Assert.AreEqual(5, Calculator.Dodaj(2, 3));
}❌ Unikaj (jeśli możliwe):
[TestMethod]
public void Dodaj_RozneScenariusze()
{
Assert.AreEqual(5, Calculator.Dodaj(2, 3));
Assert.AreEqual(10, Calculator.Dodaj(7, 3));
Assert.AreEqual(0, Calculator.Dodaj(-5, 5));
}Dlaczego? Jeśli pierwszy assert nie przejdzie, pozostałe się nie wykonają.
3. Testuj przypadki brzegowe
Nie testuj tylko „happy path”! Testuj też:
| Typ przypadku | Przykład |
|---|---|
| Wartości brzegowe | 0, -1, int.MaxValue |
| Wartości null | Przekazanie null do metody |
| Puste kolekcje | Lista z 0 elementów |
| Wyjątki | Dzielenie przez 0 |
| Niepoprawne dane | Tekst zamiast liczby |
4. Testy muszą być niezależne
Każdy test powinien działać samodzielnie. Nie polegaj na kolejności wykonywania testów!
❌ Źle:
int wynik; // Zmienna współdzielona
[TestMethod]
public void Test1() { wynik = 5; }
[TestMethod]
public void Test2() { Assert.AreEqual(5, wynik); } // Zależny od Test1!✅ Dobrze:
[TestMethod]
public void Test1()
{
int wynik = Calculator.Dodaj(2, 3);
Assert.AreEqual(5, wynik);
}
[TestMethod]
public void Test2()
{
int wynik = Calculator.Dodaj(7, 3);
Assert.AreEqual(10, wynik);
}5. Testy powinny być szybkie
⚡ Jeden test powinien wykonywać się w milisekundach, nie sekundach!
❌ Unikaj:
- Operacji na plikach (jeśli nie jest to konieczne)
- Połączeń z bazą danych
- Żądań HTTP do internetu
Thread.Sleep()
6. Używaj [TestInitialize] do przygotowania danych
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 */ }
}14. 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:
- 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.