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

wtorek, 24 marca 2020
Z moich poprzednich artykułów wiesz już, że testy automatyczne możemy podzielić między innymi na testy jednostkowe i testy integracyjne. Do testów jednostkowych wprowadziłem Cię już w ostatnich artykułach (jeżeli się z nimi jeszcze nie zapoznałeś, koniecznie to zrób). Teraz przyszła pora na testy integracyjne. W testach jednostkowych testujemy pojedyncze jednostki, bez zewnętrznych zależności. Może się okazać, że mamy mnóstwo testów jednostkowych, które są zielone, a w rzeczywistości w naszej aplikacji mogą być błędy. Możemy mieć błędy na przykład we wspomnianych wcześniej zewnętrznych zależnościach. Aby to sprawdzić, musimy również pisać testy integracyjne, które testują kilka jednostek połączonych ze sobą oraz współprace z zewnętrznymi zależnościami.

Testujemy Operacje na Bazie Danych - Wprowadzenie do Testów Integracyjnych w .NET


Dlaczego oddzielać testy jednostkowe od testów integracyjnych?


Znalazłem kilka postów w internecie, na których programiści piszą, że nie widzą sensu oddzielania testów jednostkowych od testów integracyjnych. Piszą, że test to po prostu test i nie ma sensu tego oddzielać i wprowadzać dziwnego nazewnictwa. Zazwyczaj wtedy wszystkie testy nazywają testami jednostkowymi. Nie zgadzam się z tym i w swoich projektach zawsze wprowadzam taki podział. Moim zdaniem testy jednostkowe muszą zostać oddzielane od testów integracyjnych. Przede wszystkim taki podział wprowadza porządek do mojego projektu. Po wydzieleniu dwóch typów testów do osobnych projektów wiem, że dla testów integracyjnych mam jeszcze na przykład osobną konfigurację, która musi zostać uzupełniona prawidłowa, jest też osobna baza danych, całe środowisko pod takie testy musi zostać wcześniej przygotowane. W testach jednostkowych wiem, że test zawsze musi mi zwrócić ten sam wynik i nie muszę przejmować się innymi czynnikami, które mogą wpłynąć na ten test. Poza tym testy integracyjne wykonują się dużo dłużej niż testy jednostkowe i nie są one tak często uruchamiane. Z kolei testy jednostkowe, dzięki temu, że są bardzo szybkie, możemy je wykonywać nawet przy każdym buildzie, co za tym idzie mamy szybki feedback, jeżeli coś nie działa lub mamy jakąś regresję w naszym kodzie. Jeżeli testy nie są od siebie oddzielane, to później wszystkie są uruchamiane rzadko, z czasem coraz rzadziej, ponieważ nie chcemy czekać aż tak długo na wynik, a w przypadku testów integracyjnych, to zawsze będzie trwało. Wyobrażasz sobie uruchamianie testów, które trwają kilka minut po każdym buildzie? Nie bardzo.


Przygotowanie do testów integracyjnych


Jeżeli chcemy pisać testy integracyjne, musimy przygotować sobie odpowiednie środowisko. Załóżmy, że chcemy testować jakieś metody, które zawierają w sobie zapytania lub komendy na bazie danych. W takim przypadku najlepiej sobie przygotować osobną bazę danych dla testów integracyjnych. W pliku konfiguracyjnym dla projektu z testami integracyjnymi zdefiniować osobnego connection string'a do bazy przygotowanej pod testy integracyjne. Są różne podejścia do testowania współpracy z bazą danych, między innymi testowanie na bazie danych tworzonej w pamięci, ja jednak przedstawię inne podejście. Całą logikę, która współpracuje z bazą danych, będziemy wykonywać w transakcjach i po skończeniu każdego testu zrobimy rollback transakcji. W związku z tym, że często opieramy się na zewnętrznych zależnościach, testy integracyjne wykonują się dłużej niż testy jednostkowe.


Jak wygląda przykładowy test integracyjny napisany w C#?


Załóżmy, że mamy klasę UserRepository, która, żeby za bardzo nie komplikować, tylko dodaje nowe użytkownika za pomocą entity framework.

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class UserRepository
{
    public void Add(User user)
    {
        using (var context = new ApplicationDbContext())
        {
            context.Users.Add(user);
            context.SaveChanges();
        }
    }
}

Jak widzisz, klasa ta ma zewnętrzne zależności - współpracuje z bazą danych.

#1 Baza danych dla testów integracyjnych.
Aby przeprowadzić testy w takim przypadku najlepiej sobie przygotować bazę danych dla testów integracyjnych.

#2 Nowy projekt dla testów integracyjnych.
Aby nie mieszać testów integracyjnych z innymi testami. Najlepiej założyć osobny projekt, gdzie będą tylko testy integracyjne. Dzięki takiemu podziałowi, testy jednostkowe i integracyjne mogą być uruchamiane oddzielnie. Nowy projekt możesz nazwać Project.IntegrationTests.

#3 Instalujemy wymagane frameworki.
W moim przypadku będzie to EntityFramework, NUnit oraz NUnit3TestAdapter.

#4 Konfiguracja.
W pliku app.config uzupełniam connection string'a dla mojej bazy testowej.

#5 Izolacja testów.
Chcę, żeby po każdym teście, dodane przeze mnie dane znikały z bazy danych. Aby to zrobić, wszystkie moje komendy muszą być wykonywane w transakcji. Możemy to zrobić, dodając własny atrybut, który następnie będzie dodawany do każdego testu, po którym chcemy czyścic dane. Dodajemy klasę na przykład o nazwie Isolated. Dzięki oznaczeniu naszej klasy testującej atrybutem [Isolated], przed każdym testem będzie tworzona nowa transakcja, oraz po każdym teście będzie uruchamiany rollback tej transakcji.

public class Isolated : Attribute, ITestAction
{
    private TransactionScope _transactionScope;

    public ActionTargets Targets
    {
        get { return ActionTargets.Test; }
    }

    public void AfterTest(ITest test)
    {
        _transactionScope.Dispose();
    }

    public void BeforeTest(ITest test)
    {
        _transactionScope = new TransactionScope();
    }
}

#6 Nowa klasa dla testów integracyjnych klasy UserRepository.
Trzymając się konwencji, dodajemy do projektu testowego nową klasę o nazwie UserRepositoryTests. Tutaj będą wszystkie metody, testujące klasę UserRepository.

#7 Piszemy test integracyjny.
public class UserRepositoryTests
{
    [Test, Isolated]
    public void Add_PassValidUser_ShouldAddUserToDatabase()
    {
        var context = new ApplicationDbContext();
        var user = new User { Id = 1, Name = "name" };
        var userRepository = new UserRepository();

        userRepository.Add(user);

        var usersCount = context.Users.Count(x => x.Id == user.Id && x.Name == user.Name);
        Assert.That(usersCount, Is.EqualTo(1));
    }
}

Musimy pamiętać, aby oznaczyć metodę atrybutami Test i Isolated. Dodaliśmy metodę, która po przekazaniu użytkownika, powinna dodać go do bazy danych. Najpierw jest inicjalizacja obiektów, które używamy w teście. Dodajemy użytkownika poprzez metodę Add klasy UserRepository. Na koniec sprawdzamy, czy w bazie danych jest tylko jeden użytkownik o takich danych. Test przechodzi, dodatkowo, jeżeli test odpalimy wiele razy, to zawsze będzie przechodził, ponieważ po każdym teście na bazie danych jest wykonywany rollback transakcji.


PODSUMOWANIE:


Mam nadzieję, że udało mi się Ciebie wprowadzić tym prostym przykładem w świat testów integracyjnych. Widzisz, jaka jest różnica pomiędzy testami jednostkowymi oraz integracyjnymi. Ten przykład był dość prosty, nie chciałem na początek męczyć Cię skomplikowanymi przykładami. Jeżeli będziesz zainteresowany tematem testów integracyjnych na blogu, to w kolejnych artykułach postaram się przedstawić bardziej skomplikowane przykłady.

Poprzedni artykuł - Jak Pozbywać się Zewnętrznych Zależności w Testach Jednostkowych? Wprowadzenie do Mockowania Danych w C#.
Następny artykuł - Test Driven Development: Korzyści ze stosowania TDD na przykładzie w .NET.
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 (7)
Dariusz Gruca
DARIUSZ GRUCA, wtorek, 24 marca 2020 19:58
Bardzo fajny post. Popieram w 100% wyodrębnienie testów integracyjnych do osobnego projektu. Według piramidy testów Cohana jasno wynika, że testów integracyjnych powinno być mniej niż jednostkowych. Ponieważ te testy są wolne. W jednym z projektantów przyjęliśmy zasadę, że gdy to tylko możliwe używamy bazy danych w pamięci. Bardzo fajnie się to sprawdza :) Pozdrawiam, Dariusz Gruca
M
M, środa, 25 marca 2020 11:26
Poproszę o bardziej skomplikowane przykłady :) No i super, że poruszasz tutaj te tematy testów - często jeszcze pomijane w pracy programistów, szczególnie juniorów.
Kazimierz Szpin
KAZIMIERZ SZPIN, niedziela, 7 czerwca 2020 09:36
@DARIUSZ, jasne zgadzam się, bazy danych w pamięci również dobrze się sprawdzają :)
Kazimierz Szpin
KAZIMIERZ SZPIN, niedziela, 7 czerwca 2020 09:38
@M, nie ma problemu, wkrótce dodam kolejny artykuł na temat testów integracyjnych :)
Grzegorz
GRZEGORZ, środa, 8 grudnia 2021 13:31
Witam, czy udało się dodać artykuł z bardziej zaawansowanymi przykładami?
Kazimierz Szpin
KAZIMIERZ SZPIN, czwartek, 9 grudnia 2021 10:23
@GRZEGORZ, na początek odsyłam do tagu testy integracyjne, a jeżeli szukasz więcej praktycznych materiałow, to oczywiście zapraszam do programu – Szkoła Testów Jednostkowych (https://szkolatestowjednostkowych.pl).
Damian
DAMIAN, niedziela, 22 maja 2022 15:49
Świetne wytłumaczenie jak zawsze👌 Widać po Pana kanale na YT, że Jest Pan Ekspertem :)
Dodaj komentarz

Wyszukiwarka

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