Testy jednostkowe w C# – podstawy

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ódDlaczego to jest ważne
Szybkie wykrywanie błędówOd razu wiemy, co nie działa – nie trzeba ręcznie klikać przez całą aplikację
Pewność poprawności działaniaTesty potwierdzają, że wynik jest poprawny po każdej zmianie
Łatwiejsza rozbudowa aplikacjiBez lęku dodajemy nowe funkcje – testy powiedzą nam, czy coś zepsujemy
Profesjonalne podejścieTak pracuje każda poważna firma IT
Dokumentacja koduTesty 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ą:

AtrybutZnaczeniePrzykł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 testemPrzygotowanie danych testowych
[TestCleanup]Metoda wykonywana po każdym teścieCzyszczenie zasobów

3. Instalacja i konfiguracja projektu testowego

Krok 1: Utwórz projekt główny

  1. Otwórz Visual Studio
  2. File → New → Project
  3. Wybierz Console App (.NET) lub Console App (.NET Framework)
  4. Nazwa: CalculatorApp
  5. Kliknij Create

Krok 2: Dodaj projekt testowy

  1. Kliknij prawym przyciskiem na Solution (w Solution Explorer)
  2. Add → New Project
  3. Wyszukaj: MSTest Test Project
  4. Nazwa: CalculatorApp.Tests
  5. Kliknij Create

Krok 3: Dodaj referencję do projektu głównego

To najważniejszy krok! Projekt testowy musi „widzieć” projekt główny.

  1. W Solution Explorer, rozwiń projekt CalculatorApp.Tests
  2. Kliknij prawym na Dependencies → Add Project Reference
  3. Zaznacz CalculatorApp
  4. 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.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!

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 uruchomiony

5. 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

AsercjaZnaczeniePrzykł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ć prawdziwyAssert.IsTrue(liczba > 0)
Assert.IsFalse(condition)Warunek musi być fałszywyAssert.IsFalse(tekst.IsEmpty())
Assert.IsNull(object)Obiekt musi być nullAssert.IsNull(pustyObiekt)
Assert.IsNotNull(object)Obiekt nie może być nullAssert.IsNotNull(uzytkownik)
Assert.ThrowsException<T>()Sprawdza, czy rzucono wyjątekAssert.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:

EtapZnaczenieGdzie w kodzieCo robimy
ArrangePrzygotowanie danychint a = 2; int b = 3;Tworzymy zmienne testowe
ActWykonanie testowanej metodyCalculator.Dodaj(a, b)Wywołujemy badaną funkcję
AssertSprawdzenie wynikuAssert.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

  1. Otwórz Test Explorer (Ctrl + E, T)
  2. Kliknij Run All (zielona strzałka ▶️)
  3. Poczekaj 1-2 sekundy

Możliwe wyniki

✅ Test zakończony sukcesem (zielony)

✅ Dodaj_DwieLiczbyDodatnie_ZwracaSume
   Czas: 23ms

Znaczenie: Wszystko działa poprawnie! 🎉

❌ Test nie przeszedł (czerwony)

❌ Dodaj_DwieLiczbyDodatnie_ZwracaSume
   Assert.AreEqual failed. Expected:<5>. Actual:<6>.
   Czas: 18ms

Znaczenie: 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:

  1. Postaw breakpoint (F9) w linii z asercją
  2. Kliknij prawym na test → Debug
  3. Program zatrzyma się na breakpoincie
  4. 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?

  1. [DataTestMethod] – zamiast [TestMethod]
  2. [DataRow(a, b, oczekiwany)] – każdy wiersz to jeden test
  3. MSTest uruchomi metodę 5 razy (dla każdego DataRow)
  4. 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_ZwracaSume
  • Podziel_PrzezZero_RzucaWyjatek
  • ZalogujUzytkownika_NiepoprawneHaslo_ZwracaFalse

Złe przykłady:

  • Test1, Test2 – nic nie mówią
  • TestDodawania – zbyt ogólne
  • DodajTest – 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 przypadkuPrzykład
Wartości brzegowe0, -1, int.MaxValue
Wartości nullPrzekazanie null do metody
Puste kolekcjeLista z 0 elementów
WyjątkiDzielenie przez 0
Niepoprawne daneTekst 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:

  1. 2³ = 8
  2. 5² = 25
  3. 10⁰ = 1

📝 Zadanie 2: Testowanie warunków

Dodaj metodę:

public static bool CzyParzysta(int liczba)
{
    return liczba % 2 == 0;
}

Napisz testy sprawdzające:

  1. Liczba parzysta (np. 4) → true
  2. Liczba nieparzysta (np. 7) → false
  3. Zero → true
  4. 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:

  1. Pierwiastek z 16 = 4 (test normalny)
  2. 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:

  1. Wiek 25 → true
  2. Wiek 0 → true (brzeg)
  3. Wiek 150 → true (brzeg)
  4. Wiek -1 → false
  5. 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.

Przydatne linki