Blog Dla Młodszych Programistów C#/.NET

wtorek, 11 lutego 2020
W poprzednim artykule na blogu wprowadziłem Cię do testów automatycznych. Jeżeli jeszcze go nie czytałeś, koniecznie zapoznaj się z nim, zanim przejdziesz do tego artykułu. Testy jednostkowe (unit tests), są właśnie jednym z typów testów automatycznych. W tym artykule najpierw opowiem Ci trochę o testach jednostkowych w teorii, powiem Ci jakie powinny być dobre testy jednostkowe, a następnie napiszemy pierwszy test jednostkowy, a później kolejne. Zaczynajmy!


Czym są testy jednostkowe?


Zacznijmy od teorii. Testy jednostkowe są to małe kawałki kodu, które służą do testowania innego kodu, czyli pojedynczych jednostek, to znaczy klas i metod.

Testy Jednostkowe 100% Tego, Co Musisz O Nich Wiedzieć

Jakie powinny być testy jednostkowe?


#1 Powinny być wykonywane w izolacji bez zewnętrznych zależności.
Testy jednostkowe wykonywane są w izolacji bez jakichkolwiek zewnętrznych zależności takich jak plik, baza danych, czy webserwis. Testy, które korzystają z tych zależności to testy integracyjne, które bardziej szczegółowo opiszę w przyszłych artykułach na blogu. Testy integracyjne często przez programistów, są nazywane testami jednostkowymi, jednak testy jednostkowe i integracyjne to nie to samo.

#2 Testy jednostkowe powinny być powtarzalne i niezawodne.
Testy jednostkowe dzięki temu, że nie są uzależnione od żadnych zewnętrznych zależności, zawsze powinny być powtarzalne. Czyli wynik testu zawsze powinien być taki sam. Nie ma znaczenia, kiedy uruchomimy test jednostkowy, rano czy wieczorem, na innym komputerze, rok po napisaniu, czy uruchamiany jeden test jednostkowy, czy wszystkie naraz - wynik zawsze powinien być taki sam.

#3 Testy jednostkowe powinny być szybkie.
Testy jednostkowe są uruchamiane wiele razy, właśnie ze względu na to, że są szybkie. Jeżeli Twój test wykonuje się długo, to oznacza, że prawdopodobnie korzysta z jakichś zewnętrznych zależności i nie jest testem jednostkowym.

#4 Testy jednostkowe powinny być łatwe, czytelne, jeżeli się nie powiodą, to błąd powinien być łatwy do znalezienia.
Dla testów jednostkowych powinieneś stosować odpowiednie nazewnictwo, zgodne z konwencją. Twoje testy nie powinny zawierać zbyt dużo linii kodu i trzymać się zasady AAA (Arange, Act, Assert). Jeżeli test się nie powiedzie, powinien zwrócić właściwy komunikat, mówiący o tym, co poszło nie tak.

#5 Jest napisany z użyciem frameworka do testów jednostkowych.
Jest wiele frameworków, które możesz używać. Ja proponuję wybrać jeden z dwóch: NUnit lub xUnit.net. Dzisiejsze przykłady będę przedstawiał właśnie w NUnit, chociaż xUnit.net jest równie dobry. Jedyne co odradzam, to korzystanie z frameworka MSTest, co prawda jest on dostępny bez dodawania dodatkowych frameworków, ale jest trochę ograniczony, to znaczy brakuje mu niektórych ciekawych funkcji, które są w dwóch wcześniej wspomnianych.


Pierwszy test jednostkowy w C#


Napiszmy pierwszy kod jednostkowy w C# przy użyciu NUnit. Zaczniemy od prostego przykładu, spróbujemy napisać jeden test dla metody Add w klasie Calculator. Przykładowy kod tej metody może wyglądać w ten sposób:

public class Calculator
{
    public int Add(int number1, int number2)
    {
        return number1 - number2; //celowy błąd
    }
}

#1 Dodajemy nowy projekt z testami.
Najpierw należy dodać nowy projekt do naszej solucji. Zgodnie z konwencją może to być projekt o nazwie Calculations.UnitTests. W tym projekcie będą nasze testy jednostkowe. Najlepiej testy jednostkowe oddzielić od kodu produkcyjnego i testów integracyjnych.

#2 Do projektu z testami dodajemy referencję do projektu testowanego.
Można to zrobić na kilka sposobów. Na przykład klikając prawym przyciskiem na Calculations.UnitTests, następnie Add oraz Reference. Po otwarciu nowego okna z zakładki Project/Solution wybieramy projekt testowany, czyli w naszym przypadku Calculations.

#3 Instalujemy NUnit.
Przechodzimy do Manage NuGet Package i instalujemy dla projektu z testami NUnit.

#4 Dodajemy nową klasę odpowiadającą klasie testowanej.
W projekcie z testami dodajemy nową klasę, która będzie odpowiedzialna za testowanie klasy Calculator. Zgodnie z konwencją nazywamy ją CalculatorTests. W tej klasie będą dodawane metody testujące klasę Calculator. Dla każdej kolejnej klasy testowanej zawsze najlepiej dodać osobną klasę testującą, dzięki temu nasz kod staje się bardziej przejrzysty.

#5 Dodajemy pierwszą metodę testową.
Załóżmy, że na początek chcemy przetestować tak zwaną szczęśliwą ścieżkę (happy path), czyli testujemy metodę dla liczb, które powinny zachować się prawidłowo. Taka metoda może wyglądać mniej więcej tak:

public class CalculatorTests
{            
    [Test]
    public void Add_WhenCalled_ShouldReturnSum()
    {
        //Arrange
        var calculator = new Calculator();

        //Act
        var result = calculator.Add(1, 2);

        //Assert
        Assert.AreEqual(3, result);
    }
}

Nazywamy metodę według konwencji nazewniczej:

MetodaTestowana_Scenariusz_OczekiwaneZachowanie

Testujemy metodę Add, dla dwóch liczb i oczekujemy poprawnego wyniku, czyli sumy tych liczb. Aby metoda była testem, musi zostać oznaczona atrybutem [Test]. Często programiści oznaczają również klasę atrybutem [TestFixture], wynika to z tego, że w starszych wersjach NUnit było to wymagane, ale obecnie od wersji 2.5 nie jest to potrzebne. Następnie w ciele metody dzielimy nasz kod na trzy części: AAA, czyli Arrange, Act, Assert. W arrange jest tak zwane przygotowanie, czyli najczęściej są tutaj inicjalizowane obiekty, w naszym przykładzie tworzymy obiekt calculator. W act jest działanie, czyli wykonujemy metodę, którą chcemy testować, wywołujemy metodę Add z odpowiednimi parametrami. W assert sprawdzamy, czy wynik oczekiwany jest równy faktycznemu wynikowi. Klasa Assert jest z frameworka NUnit i zawiera jeszcze dużo więcej innych metod statycznych do weryfikowania wyników naszych testów. Taki podział metod jest często stosowany, dzięki niemu metoda testowa staje się przejrzysta. Oczywiście komentarze nie są tutaj potrzebne, można je usunąć, zostawiłem je tylko dla przejrzystości tego przykładu.

#6 Instalujemy NUnit3TestAdapter.
Jeżeli nie posiadamy ReSharpera, to aby można było uruchomić testy, musimy zainstalować poprzez Manage NuGet Package jeszcze jeden składnik - NUnit3TestAdapter.

#7 Uruchomienie testu.
Testy można uruchamiać na różne sposoby. Jeżeli używasz w visual studio to na początek w zakładce View kliknij w Test Explorer, dzięki temu pojawi Ci się okno z testami. Następnie widzisz strukturę Twoich testów, możesz uruchomić jeden test, możesz też uruchamiać wszystkie. Wystarczy kliknąć prawym przyciskiem myszy na nazwę testu i kliknąć Run. Testy są odpalane często, więc najlepiej jak w przyszłości będziesz używał do tego skrótów klawiszowych. Możesz użyć domyślnego lub dodać swój własny. Jak widzisz w test explorer nasz test zaświecił się na czerwono, to oznacz błąd w metodzie Add. Po kliknięciu w metodę testującą w test explorer możesz zobaczyć wiadomość: "Expected: 3 But was: -1". Celowo zrobiłem błąd w naszym przykładzie, który udało nam się wykryć w teście. Poprawiamy kod metody Add w klasie Calculator.

public class Calculator
{
    public int Add(int number1, int number2)
    {
        return number1 + number2;
    }
}

Ponownie uruchamiany test i teraz test świeci się na zielono. Oznacza to, że test przeszedł pozytywnie.

Super, napisaliśmy pierwszy test. Powyższy przykład nie był zbyt skomplikowany, myślę jednak, że na początek warto od takiego zacząć, tak aby przejść przez cały proces. W naszym przykładzie sprawdziliśmy tylko jeden przypadek testowy, ale zazwyczaj jak testujemy, to musimy sprawdzić więcej ścieżek.

Sprawdzamy wszystkie warunki testowe, jeżeli w metodzie są jakieś instrukcje warunkowe, to wtedy liczba przypadków testowych rośnie. Jeżeli chcemy utrwalić wiedzę, potrzebujesz więcej praktyki. W tym celu napiszemy test jednostkowy w C# do słynnego algorytmu FizzBuzz. Słynnego, ponieważ, często na rozmowach kwalifikacyjnych rekruterzy proszą o jego zaimplementowanie :)

Co to jest FizzBuzz?
Jest to prosty algorytm, który na podstawie przekazanego argumentu zwraca odpowiednią wartość. Zasady są takie:
  • Jeżeli argument jest podzielny przez 3, zwraca Fizz,
  • Jeżeli argument jest podzielny przez 5, zwraca Buzz,
  • Jeżeli argument jest podzielny przez 3 i przez 5, zwraca FizzBuzz,
  • Jeżeli argument nie jest podzielny przez 3 ani przez 5, zwraca wartość argumentu.
Implementacja algorytmu może wyglądać mniej więcej tak:

namespace TestsDotNet
{
    public class FizzBuzz
    {
        public string GetOutput(int number)
        {
            if ((number % 3 == 0) && (number % 5 == 0))
                return "FizzBuzz";

            if (number % 3 == 0)
                return "Fizz";

            if (number % 5 == 0)
                return "Buzz";

            return number.ToString();
        }
    }
}


Przykład testu jednostkowego w .NET


Nasze testy jednostkowe napiszemy przy użyciu biblioteki NUnit. Zgodnie z krokami, które zostały szczegółowo opisane powyżej:
  • Dodajemy nowy projekt z testami, będzie to TestsDotNet.UnitTests,
  • Do projektu z testami TestsDotNet.UnitTests, dodajemy referencję do projektu testowanego, w tym przypadku TestsDotNet,
  • Instalujemy NUnit poprzez NuGeta,
  • Instalujemy NUnit3TestAdapter poprzez NuGeta.
  • Dodajemy klasę testową o nazwie FizzBuzzTests.
Teraz przez chwilę, zastanówmy się na spokojnie, co konkretnie chcemy przetestować, a następnie zdefiniujmy przypadki testowe, które chcemy przetestować. Tutaj możemy się posłużyć wymaganiami zdefiniowanymi powyżej. Zaczynając od pierwszego - jeżeli argument jest podzielny przez 3, zwraca "Fizz". Stosując odpowiednią konwencję nazewnictwa, to jest:
MetodaTestowana_Scenariusz_OczekiwaneZachowanie
Możemy nazwać naszą metodę:
GetOutput_WhenInputIsDivisibleOnlyBy3_ShouldReturnFizz
Nie zapomnijmy o oznaczeniu metody atrybutem [Test].

[Test]
public void GetOutput_WhenInputIsDivisibleOnlyBy3_ShouldReturnFizz()
{
}

Piszemy nasz test zgodnie z zasadą AAA - Arrange, Act, Assert. Najpierw w Arrange, przygotujemy nasz obiekt do testów. Chcemy testować metodę klasy FizzBuzz, więc zainicjalizujmy najpierw ten obiekt.

var fizzbuzz = new FizzBuzz();

W Act następuje działanie, czyli uruchamiamy metodę GetOutput z odpowiednim parametrem - według scenariusza i zwracamy wartość do zmiennej na przykład o nazwie result.

var result = fizzbuzz.GetOutput(3);

W Assert weryfikujemy czy rzeczywiście w zmiennej result jest to, czego oczekujemy, czyli "Fizz". Możemy tutaj skorzystać z klasy Assert frameworka NUnit, a konkretnie metody statycznej That. Czyli weryfikujemy, że result czyli to, co zwróciła metoda GetOutput(3) - jest równe "Fizz".

Assert.That(result, Is.EqualTo("Fizz"));

I gotowe, napisaliśmy test do 1 scenariusza :) Nasza pierwsza metoda testująca, będzie wyglądać w ten sposób:

[Test]
public void GetOutput_WhenInputIsDivisibleOnlyBy3_ShouldReturnFizz()
{
    var fizzbuzz = new FizzBuzz();

    var result = fizzbuzz.GetOutput(3);

    Assert.That(result, Is.EqualTo("Fizz"));
}

Następnie piszemy test dla kolejnego scenariusza, tym razem oczekujemy, że dla parametru 5, zostanie zwrócony wynik "Buzz". Nasz test będzie wyglądał bardzo podobnie, zmieniamy tylko argument na 5, oczekiwany wynik w tym przypadku powinien być "Buzz":

[Test]
public void GetOutput_WhenInputIsDivisibleOnlyBy5_ShouldReturnBuzz()
{
    var fizzbuzz = new FizzBuzz();

    var result = fizzbuzz.GetOutput(5);

    Assert.That(result, Is.EqualTo("Buzz"));
}

Musimy jeszcze zweryfikować czy dla argumentu podzielnego przez 3 i 5 zostanie zwrócone "FizzBuzz", ale to już wiesz jak zrobić.

[Test]
public void GetOutput_WhenInputIsDivisibleBy3And5_ShouldReturnFizzBuzz()
{
    var fizzbuzz = new FizzBuzz();

    var result = fizzbuzz.GetOutput(15);

    Assert.That(result, Is.EqualTo("FizzBuzz"));
}

A także, czy dla argumentu nie podzielnego przez 3 ani przez 5, zostanie zwrócony ten sam argument. Tutaj nie ma co szukać jakichś magicznych liczb, jako argument możemy podstawić 1, czyli liczbę, która nie jest podzielna przez 3 ani przez 5. Jeżeli podstawimy jakąś liczbę typu 679, to w przyszłości jak wrócimy do tego testu, to będziemy tracić czas i zastanawiać się co akurat taka liczba mogła w tym przypadku oznaczać :) Czy to jakaś szczególna liczba, czy po prostu losowa, która nie jest podzielna przez 3 ani przez 5.

[Test]
public void GetOutput_WhenInputIsNotDivisibleBy3Or5_ShouldReturnInput()
{
    var fizzbuzz = new FizzBuzz();

    var result = fizzbuzz.GetOutput(1);

    Assert.That(result, Is.EqualTo("1"));
}

Uruchamiamy nasze testy i w test explorze powinien się pojawić zielony kolor, oznaczający, że nasze testy przeszły pozytywnie, nasz kod produkcyjny działa prawidłowo - wdrażamy na produkcję :)


Klasa FizzBuzzTests:


using NUnit.Framework;

namespace TestsDotNet.UnitTests
{
    public class FizzBuzzTests
    {
        [Test]
        public void GetOutput_WhenInputIsDivisibleOnlyBy3_ShouldReturnFizz()
        {
            var fizzbuzz = new FizzBuzz();

            var result = fizzbuzz.GetOutput(3);

            Assert.That(result, Is.EqualTo("Fizz"));
        }

        [Test]
        public void GetOutput_WhenInputIsDivisibleOnlyBy5_ShouldReturnBuzz()
        {
            var fizzbuzz = new FizzBuzz();

            var result = fizzbuzz.GetOutput(5);

            Assert.That(result, Is.EqualTo("Buzz"));
        }

        [Test]
        public void GetOutput_WhenInputIsDivisibleBy3And5_ShouldReturnFizzBuzz()
        {
            var fizzbuzz = new FizzBuzz();

            var result = fizzbuzz.GetOutput(15);

            
            Assert.That(result, Is.EqualTo("FizzBuzz"));
        }

        [Test]
        public void GetOutput_WhenInputIsNotDivisibleBy3Or5_ShouldReturnInput()
        {
            var fizzbuzz = new FizzBuzz();

            var result = fizzbuzz.GetOutput(1);

            Assert.That(result, Is.EqualTo("1"));
        }
    }
}

Wynik w test explorer:

FizzBuzz - wynik testów


Szkoła Testów Jednostkowych


Jeżeli ten artykuł Cię zainteresował i chcesz nauczyć się pisać testy jednostkowe, tak żeby pisać lepszej jakości kod, to koniecznie zapoznaj się ze Szkołą Testów Jednostkowych:
  • Poznasz Cały Proces Pisania Testów Jednostkowych Dla Programistów C#/.NET.
  • Otrzymasz 9 Tygodni Szkolenia Online W Formie Video.
  • Przejdziesz Przez Proces Pod Okiem Mentora.
  • Dużo praktyki.
  • Poznasz najczęstsze błędy początkujących.
  • Otrzymasz Bonusy.
Kliknij tutaj by dowiedzieć się więcej >>> SzkołaTestówJednostkowych.pl


PODSUMOWANIE


W dzisiejszym artykule pokazałem Ci, jak wygląda cały proces testowania pojedynczej metody. Wprowadziłem Cię w teorię, napisaliśmy pierwszy test jednostkowy w .NET. Pokazałem Ci cały proces pisania testu jednostkowego. Przeanalizowaliśmy różne przypadki testowe i najczęściej właśnie tak to wygląda w naszych aplikacjach. Rozpisaliśmy konkretne przypadki testowe, czyli scenariusze, które musimy sprawdzić, a następnie napisaliśmy do nich testy. Wszystkie nasze testy przeszły pozytywnie, dzięki temu wiemy, że nasz program działa według założeń. Mając już napisane testy, jeżeli w przyszłości okaże się, że coś trzeba zmienić w naszej metodzie, zrefaktoryzować kod, lub dodać nową funkcjonalność - mamy pewność, że nie spowoduje to żadnej regresji, bo wówczas zostanie ona wykryta przez testy jednostkowe. Ten przykład był o tyle prosty, że nie było w nim żadnych zewnętrznych zależności. O tym, jak radzić sobie z zewnętrznymi zależnościami w testach jednostkowych opiszę Ci w kolejnym artykule. Do zobaczenia!

Poprzedni artykuł - Testy Automatyczne Wyjaśnione w Jednym Artykule.
Następny artykuł - Jak Pozbywać się Zewnętrznych Zależności w Testach Jednostkowych? Wprowadzenie do Mockowania Danych w C#.
Autor artykułu:
Kazimierz Szpin
Kazimierz Szpin
CTO & Founder - FindSolution.pl
Programista C#/.NET. Specjalizuje się w Blazor, ASP.NET Core, ASP.NET MVC, ASP.NET Web API, WPF oraz Windows Forms.
Autor bloga ModestProgrammer.pl
Komentarze (4)
Marcin
MARCIN, piątek, 5 czerwca 2020 11:16
Nie do końca zgadzam się się z konwencją nazewnictwa "MetodaTestowana_Scenariusz_OczekiwaneZachowanie", pierwotnie wprowadzonej przez Roy'a Osherove. Wg mnie wprowadzanie nazwy metody do nazwy metody testującej w późniejszym czasie może przerodzić się w dodatkowy koszt podczas refaktoryzacji. Jeżeli np. metodę Add zmienimy później na AddNumbers, musimy także poprawić nazwę testu. Oczywiście sądzę, że tzw. "wspomagacze" typu ReSharper nie miałyby z tym problemu, ale nie wszyscy korzystają z tego typu narzędzi. Dlatego jestem zwolennikiem używania "plain English" w nazwie testu. Testy jednostkowe powinny testować "observable behaviour", wykorzystanie nazwy metody pokazuje nam testowanie szczegółów implementacyjnych, co poniekąd jest niepoprawnym zachowaniem. Polecam fajny wpis na blogu Vladimira Khorikova https://enterprisecraftsmanship.com/posts/you-naming-tests-wrong/ :)
Łukasz
ŁUKASZ, sobota, 6 czerwca 2020 01:04
Witam serdecznie, Czy funkcja GetOutput celowo jest statyczna ? public static string GetOutput(int number)
Kazimierz Szpin
KAZIMIERZ SZPIN, niedziela, 7 czerwca 2020 09:51
@MARCIN, jasne, masz rację, są różne podejścia do nazywania testów. Mi najbardziej odpowiada podejście Roy'a i tego się trzymam. Każde nazewnictwo ma swoje plusy i minusy, najważniejsze, żeby ustalić w team'ie jedną spójna konwencję i tego się trzymać :)
Kazimierz Szpin
KAZIMIERZ SZPIN, niedziela, 7 czerwca 2020 09:55
@ŁUKASZ, może być statyczna, ale w tym przypadku akurat nie powinna, moje niedopatrzenie. Już poprawiłem, dzięki :)
Dodaj komentarz

Wyszukiwarka

© Copyright 2024 modestprogrammer.pl. Wszelkie prawa zastrzeżone. Regulamin. Polityka prywatności. Design by Kazimierz Szpin