Testy jednostkowe w C# – MSTest

Od podstaw testowania, przez asercje i wzorzec AAA, aż po testy parametryzowane i testowanie wyjątków.

MSTest Assert Unit Tests AAA Pattern DataRow
01

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ó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ściTesty potwierdzają, że wynik jest poprawny po każdej zmianie
Łatwiejsza rozbudowaBez lęku dodajemy nowe funkcje – testy powiedzą, 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 – kalkulator

❌ Bez testów:

1
Piszesz funkcję
2
Uruchamiasz
3
Wpisujesz ręcznie
🔁
Powtarzasz…

✅ Z testami:

1
Piszesz funkcję
2
Piszesz test
3
Klikasz „Run”
Gotowe!
02

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

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
03

Konfiguracja projektu testowego

Krok 1: Utwórz projekt główny

  1. Otwórz Visual Studio
  2. File → New → Project
  3. Wybierz Console App (.NET)
  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. Bez tego testy nie znajdą klas do testowania!

  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

Solution 'CalculatorApp’
  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:

cs/CalculatorTests.cs C#
using CalculatorApp;  // Jeśli się podkreśla na czerwono = brak referencji!
04

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 not run (szary)

Test nie został jeszcze uruchomiony. Kliknij „Run All” aby uruchomić wszystkie testy.

05

Co to jest asercja?

Asercja (ang. assertion) to instrukcja sprawdzająca, czy wynik działania programu jest taki, jak oczekiwany.

cs/Asercja.cs C#
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ć trueAssert.IsTrue(liczba > 0)
Assert.IsFalse(condition)Warunek musi być falseAssert.IsFalse(lista.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>()
Własny komunikat błędu

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!");

06

Kod programu do testowania

Stwórzmy prostą klasę Calculator w projekcie głównym.

cs/Calculator.cs C#
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}";
        }
    }
}
07

Pierwszy test – wzorzec AAA

Każdy dobry test składa się z trzech sekcji: Arrange → Act → Assert.

Arrange
przygotuj dane
Act
wykonaj metodę
Assert
sprawdź wynik
cs/CalculatorTests.cs C#
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);
        }
    }
}
Dlaczego AAA?

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

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

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 – możesz podejrzeć wartości zmiennych
08

Różne przypadki testowe

Dobry programista testuje wiele scenariuszy, nie tylko „happy path” (idealny przypadek).

Test z liczbami ujemnymi

cs/TestUjemne.cs C#
[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

cs/TestZero.cs C#
[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

cs/TestMnozenie.cs C#
[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);
}
09

Testowanie wyjątków

Czasami oczekujemy, że kod RZUCI WYJĄTEK (np. dzielenie przez zero). MSTest pozwala to przetestować!

Przykład: dzielenie przez zero

cs/TestWyjatek.cs C#
[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);
    });
}
Jak to działa?

() => { } 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)

cs/TestDelta.cs C#
[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);
}
Delta – tolerancja błędu

Przy liczbach zmiennoprzecinkowych używaj trzeciego parametru (delta), bo może być błąd zaokrąglenia! 0.001 oznacza tolerancję ±0.001.

10

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)]

cs/TestParametryzowany.cs C#
[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!
Zalety parametryzacji

✅ Mniej kodu
✅ Łatwo dodać nowe przypadki testowe
✅ Wszystkie testy widoczne w Test Explorer osobno

11

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ć!

cs/TestKomunikat.cs C#
[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

cs/TestKomunikatDataRow.cs C#
[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);
}
12

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.

🔧 Rozwiązanie

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!

Zasada

Każdy test musi działać niezależnie. Nie polegaj na kolejności wykonywania testów!

13

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

Co to są 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 przypadkuPrzykładDlaczego ważne?
Wartości brzegowe0, -1, int.MaxValueGranice zakresu często powodują błędy
Wartości nullPrzekazanie nullNajczęstszy wyjątek to NullReferenceException
Puste kolekcjeLista z 0 elementów, pusty stringPętle mogą się zachować nieprzewidywalnie
Jeden elementLista z 1 elementemAlgorytmy sortowania mogą mieć błędy
Znaki specjalneSpacje, emotki, polskie znakiProblemy z kodowaniem
Jak myśleć o przypadkach brzegowych?

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!

❌ Unikaj w testach

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

cs/TestInitialize.cs C#
[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

Zadania praktyczne

📝 Zadanie 1: Podstawowe testy

Dodaj do klasy Calculator metodę:

cs/Poteguj.cs C#
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ę:

cs/CzyParzysta.cs C#
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ę:

cs/Pierwiastek.cs C#
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ę:

cs/CzyWiekPoprawny.cs C#
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:

cs/StringHelper.cs C#
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