Testy integracyjne. Co i jak?

Odpowiedz Nowy wątek
2019-05-13 11:00
0

Hej, to moje pierwsze podejście do testów integracyjnych i chciałbym się dowiedzieć jak to prawidłowo robić i jakie mam błędy w rozumieniu tematu. Sporo na ten temat przeczytałem, ale nie wszystko jest dla mnie jasne.

Z testami jednostkowymi mam prosto:
na każdy projekt (który podlega testom jednostkowym) jest jeden projekt testowy. Jeden plik testuje jedną klasę, np:

- solution
    - source
        + ModelsProject
        + ClientProject
    - tests
      - unitTests
        - ModelsTestProject
            Model1Tests.cs
            Model2Tests.cs
            Model3Tests.cs
        - ClientTestProject
          ...

Do takiej hierarchii można to uprościć. To mi się sprawdza, jest przejrzyste i działa. I super. Jednak teraz chciałbym dołożyć testy integracyjne i nie za bardzo wiem, jak się do tego zabrać. Rozumiem, że tutaj działamy na żywym organizmie. Mockowanie ograniczam właściwie jedynie do interakcji z użytkownikiem (np. okna dialogowe). Baza danych to SQLite w pamięci. Tylko nie za bardzo teraz wiem, jak to zrobić dobrze. Miałem już kilka podejść, jednak za każdym razem mam nieodparte wrażenie, że coś robię źle.

Ja wiem, że to zależy głównie od systemu, ale jak tutaj powinna wyglądać taka hierarchia projektów i klas? No bo projekt testowy na jeden projekt w solucji (tak jak mam przy testach jednostkowych) w tym momencie jest bez sensu. Bo projekty współdziałają ze sobą. Więc Wpadłem na pomysł, żeby dać różne projekty na testowanie różnych klientów. Np: WpfIntegrationTests, XamarinIntegrationTests, WebIntegrationTests... Wydaje się to logiczne. Więc idźmy dalej. Co tak naprawdę powinienem testować? W tym momencie zacząłem od testów WpfClient i MainViewModelu. Jednak szybko okazało się, że w MainViewModel nie mam tak naprawdę za wiele do testowania, bo większość akcji jest na poziomie innych viewmodeli. Ale to chyba nie jest problem.

Kolejnym problemem jest co i jak testować. Przykładowo podczas unit testów tworzę sobie metody w standardowym stylu:

public void AddItem_ItemIsNull_ThrowArgumentNullException()
{
//
}

public void AddItem_ItemIsNotNull_AddsItem()
{
//
}

public void AddItem_ItemIsNotNull_AddsItem()
{
//
}

itd. czyli testuję brzegowe wartości, jakieś wartości, które są ok i jakieś wartości, które są nie ok (np. item = null).

Jeśli chodzi o testy integracyjne, to czytam różne rzeczy. Że tu testujemy np. tylko happy path. Czy to jest reguła? Czy też powinienem testować podanie np. niepoprawnych wartości? No i jak nazywać takie metody?

public void CreateDocumentTest()
{
//
}

public void SaveDocumentTest()
{
//
}

public void LoadDocumentTest()
{
//
}

Czy jakoś inaczej? Jak to robicie? Jakie macie doświadczenia i jakie systemy sobie wypracowaliście? I czy używacie do tego innej biblioteki niż do jednostkowych? Ja do wszystkiego używam nUnit.

Pozostało 580 znaków

2019-05-14 10:15
1
Gworys napisał(a):

Trochę mi to przypomina porównywanie repozytoriom do DAO.
A czym twoim zdaniem różni się Stub od Mock. Może rzeczywiście, jeśli nie widać różnicy to nietrzeba używać innej nazwy...?

Nie widzę potrzeby granulacji nazw dla testowej implementacji czegoś. W myśl zasady: "Nie należy mnożyć bytów ponad potrzebę".
Nie wykluczam, że żyję w jaskini bez światła, a Ty przynosisz mi latarkę ;-)

Skorzystałem, z okazji, że wspomniałeś o tym, że ludzie tego nie rozróżniają, dlatego dopytuję o korzyści płynące z takiego podejścia ewentualnie skąd ta potrzeba granulacji.

Pozostało 580 znaków

2019-05-14 10:30
0
yarel napisał(a):
Gworys napisał(a):

Trochę mi to przypomina porównywanie repozytoriom do DAO.
A czym twoim zdaniem różni się Stub od Mock. Może rzeczywiście, jeśli nie widać różnicy to nietrzeba używać innej nazwy...?

Nie widzę potrzeby granulacji nazw dla testowej implementacji czegoś. W myśl zasady: "Nie należy mnożyć bytów ponad potrzebę".
Nie wykluczam, że żyję w jaskini bez światła, a Ty przynosisz mi latarkę ;-)

Skorzystałem, z okazji, że wspomniałeś o tym, że ludzie tego nie rozróżniają, dlatego dopytuję o korzyści płynące z takiego podejścia ewentualnie skąd ta potrzeba granulacji.

Wynika z tego, iż są to dwa byty o innym zastosowaniu, które w niczym siebie nie przypominają (oprócz tego, że są tworzone na potrzeby testów).

Jeśli nie potrzebujesz takiego podziału to po prostu nie używaj postfiksu, który może wprowadzać kogoś w błąd lub powodować problem ze zrozumieniem twojej intencji twórczej.


Unhandled Exception: System.MissingMethodException: Constructor on type 'System.Exception' not found.

Pozostało 580 znaków

2019-05-14 11:25
0

Dobra, trochę odbiegamy od tematu. Czuję się jakbym rzucił kiełbasą w stado wilków :D
Kilka razy bywało, że uważałem, że @jarekr000000 opowiada głupoty, jednak w większości przypadków później przyznawałem mu rację. Albo chociaż częściową rację. Dlatego też chciałbym Cię prosić o jakąś kontynuację tematu. Tak samo @somekind, którego uważam za człowieka z dużą wiedzą (nie mówię, że inni jej nie mają, po prostu bacznie zawsze czytam posty tej dwójki).

Nie rozumiem za bardzo jak można nie odróżniać testów (tak zrozumiałem z wypowiedzi Jarka). Przecież główną zasadą testów jednostkowych jest testowanie elementów systemu w izolacji. Natomiast testy integracyjne mają zupełnie inny zamysł. Więc pytanie na jakiej płaszczyźnie nie rozróżniasz tych testów?

W ogóle chciałbym najlepiej zobaczyć jakieś Wasze praktyczne przykłady testów. O ile z testami jednostkowymi wydaje mi się, że nie mam problemów (zazwyczaj), to właśnie natrafiłem na pewien opór przy testach integracyjnych. I czytając wszystkie posty tak naprawdę nie wiem sam, czy robię w tym momencie testy integracyjne, funkcyjne, czy może akceptacyjne, bo teoretycznie wszystko by mi pasowało do tego, co robię teraz. Tu mogę zrozumieć nierozróżnianie tych testów, bo granica wydaje mi się dość cienka i płynna.

Wg mnie wystarczą 2 rodzaje testów, a 3 są ideałem:

  1. Testy jednostkowe
  2. Testy integracyjne (zwał jak zwał - testujące system jako całość)
  3. Testy przez człowieka

Podejrzewam też, że tak jest w większości przypadków. Dlatego pożądam Waszych kodów :D Serio, chciałbym zobaczyć jak piszecie testy, możecie coś pokazać? To nie musi być nic prawdziwego, to może być jakiś zmyślony system.

Pozostało 580 znaków

2019-05-14 13:22
0

Tu mam taki trochę złośliwy przykład:
https://github.com/javaFunAga[...]repo/UsersRepositoryTest.java

W środku masz jeden zestaw testów UserRepositoryBase
Odpalany raz z HashMapą w pamięci UsersRepositoryTest.
A raz z faktyczną bazą (nie SQL) UserPersistentRepoTest.

Pozwiedz teraz jaki to rodzaj testów...

@Test
    public void shouldRegisterUserOnlyOnce() {
        this.usersRepository.addUser("irreg", "bbbb");
        final RegUserStatus result = this.usersRepository.addUser( "irreg" , "baaaa");
        assertThat ( result.ok, is( false));
    }

Sprawdzamy, że usera można rejestrować tylko raz (z danym loginem).

Przecież główną zasadą testów jednostkowych jest testowanie elementów systemu w izolacji.

Otóż uważam, że jest to niepraktyczne. Bo w izolacji od czego? Praktycznie zawsze zależysz od runtime, bibliotek, reszty modułów.
Przez nadużywanie tej koncepcji izolacji mam potem testy, które testują tylko mockito.


Bardzo lubie Singletony, dlatego robię po kilka instancji każdego.
edytowany 2x, ostatnio: jarekr000000, 2019-05-14 14:13

Pozostało 580 znaków

2019-05-14 13:37
2

Izolacja w testach jest ważna, ale w tym sensie, że testy powinny być od siebie nawzajem odizolowane, a więc nie powinny w żaden sposób na siebie wpływać. Stąd wynika iż jeśli np mamy globalny mutowalny stan, który jest zmieniany w testach to by testy były odizolowane muszą być wykonywane jednowątkowo, a globalny stan musi być za każdym razem przywracany do jednakowego stanu. Izolacja w takim scenariuszu może być bardzo trudna jeżeli jest sporo rzeczy, które dzieją się asynchronicznie. Projekt nad którym pracuję w robocie ma właśnie taki problem. Niektóre mikroserwisy są testowane tak, że jest globalny mutowalny stan, ograniczenie na jeden test jednocześnie na jedną JVMkę, resetowanie globalnego stanu między testami, a mimo tego izolacja nie działa i nie wiadomo dlaczego. Coś gdzieś jest pominięte przy resetowaniu stanu czy ubijaniu asynchronicznych zadań. Dążymy do tego by pozbyć się takich mikroserwisów i takiego projektowania kodu (gdzie izolacja jest trudna do osiągnięcia).

Często jednak izolację rozważa się w innym sensie. Nie izolację między testami, ale np izolację między różnie rozumianymi jednostkami.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 2x, ostatnio: Wibowit, 2019-05-14 13:41

Pozostało 580 znaków

2019-05-14 14:30
2

na przykładzie web api, 2 poziomy testów:
1) test endpointa, szczęśliwe ścieżki + wszystkie inne do których ktoś by próbował ręcznie pisać żądania w Postmanie
2) test serwisów aplikacyjnych, przypadki brzegowe

poziom 1)

[TestClass]
public class Questions_HappyPath : BaseFixture
{
    private const string EndpointName = "Questions";
    private HttpClient client;

    [TestInitialize]
    public void TestInitialize()
    {
        factory = new ApiFactory(ApiFactory.DatabaseType.SQLiteInMemory);
        client = factory.CreateClient(Token);
    }

    [TestMethod]
    [DataRow(1, 1)]
    [DataRow(2, 2)]
    [DataRow(3, 4)]
    public async Task UpdateQuestionWithAnswers(long questionId, long answerId)
    {
        var command = new UpdateQuestion
        {
            Content = "Who is your momy?",
            Answers = new List<UpdateAnswer>
            {
                new UpdateAnswer { AnswerId = answerId, Content = "Alicia", IsCorrect = false }
            },
            CatalogId = 2,
            ConcurrencyToken = 0
        };

        var response = await client.PutAsync($"{EndpointName}/{questionId}/", command);

        response.EnsureSuccessStatusCode();
        var context = factory.GetContext<TestCreationDbContext>();
        var actualQuestion = context.Questions.Include(x => x.Answers).FirstOrDefault(x => x.QuestionId == questionId);

        AssertExt.AreEquivalent(command, actualQuestion);
    }
}

poziom 2)

public class QuestionsServiceTests : BaseFixture
{
    private TestCreationDbContext TestCreationDbContext;
    private QuestionsService serviceUnderTest;

    private protected override DatabaseType GetDatabaseType()
    {
        return DatabaseType.SQLiteInMemory;
    }

    [TestInitialize]
    public void TestInitialize()
    {
        using (var context = CreateTestCreationDbContext())
        {
            TestUtils.Seed(context);
        }
        TestCreationDbContext = CreateTestCreationDbContext();
        var uow = TestUtils.CreateTestCreationUoW(TestCreationDbContext);
        serviceUnderTest = new QuestionsService(new QuestionReader(TestCreationDbContext), uow);
    }
    [TestCleanup]
    public void TestCleanup()
    {
        TestCreationDbContext.Dispose();
    }

    [TestMethod]
    [DataRow(ValidQuestionId, ResultStatus.Ok)]
    [DataRow(DeletedQuestionId, ResultStatus.NotFound)]
    [DataRow(NotExisitngQuestionId, ResultStatus.NotFound)]
    [DataRow(OtherUserQuestionId,   ResultStatus.Unauthorized)]     
    public void UpdateQuestionWithAnswers(long questionId, ResultStatus expectedResult)
    {
        var command = new UpdateQuestion()
        {
            Content = "Dani Carvajal",
            CatalogId = ValidQuestionsCatalogId,
            ConcurrencyToken = 0,
        };
        Result result = serviceUnderTest.UpdateQuestionWithAnswers(OwnerId, questionId, command);
        Assert.AreEqual(expectedResult, result.Status);
    }

    [TestMethod]      
    public void UpdateQuestionWithAnswers_GivenWrongConcurrencyToken_ShouldReturnResultConflict()
    {
        var command = new UpdateQuestion()
        {
            Content = "Dani Carvajal",
            CatalogId = ValidQuestionsCatalogId,
            ConcurrencyToken = 666,
        };
        Result result = serviceUnderTest.UpdateQuestionWithAnswers(OwnerId, ValidQuestionId, command);
        Assert.AreEqual(ResultStatus.Conflict, result.Status);
    }
}

#Dżunior React Devloper wanna be#
O co ci chodzi z tym pierwszym poziomem.? - Gworys 2019-05-14 18:46
próba uniknięcia wdawania się w dyskusje integracyjne vs jednostkowe - neves 2019-05-14 19:00
To się nie odzywam :P - Gworys 2019-05-14 19:10

Pozostało 580 znaków

2019-05-14 17:08
4

Ok to zacznijmy od początku...
Po co w ogóle testujesz? Chcesz wiedzieć, że coś działa prawidłowo. I jednocześnie nie chcesz tego sprawdzać ręcznie za każdym razem. Teraz jak to zrobisz... Logikę która jest ładnie wydzielona, można łatwo i szybko przetestować w izolacji w pamięci - tu testy jednostkowe błyszczą. I na tym skończę o jednostkowych bo napisałeś że tu jest super. Super.

Testy integracyjne z natury są wolniejsze. Często dotykają zewnętrznych zależności jak baza danych czy serwisy trzecich firm (zwykle testowe). To czy użyjesz mocka takiego serwisu, czy nie, będzie zależało od jego stabilności oraz Twoich potrzeb (uruchomienie testów bez dostępu do internetu). Czasem robi się dwa zestawy wtedy, jeden korzysta z np test api paypala, które częściej nie działa jak działa, i osobny zestaw je mockuje. Trudno, takie życie.
Z bazą danych podobnie. Zależy czego oczekujesz. Jeśli chcesz mieć 100% pewności że wszystko działa na np MySQL to nie użyjesz sqllite. Jeśli uważasz że sprawdzenie na sqlite wystarczy, a resztę się "przeklika" czy np testy e2e z UI sprawdzą - to zależy od Ciebie.
Testy integracyjne, też dają Ci świadomość żę nie tylko poszczególne kawałki działają, ale ładnie się ze sobą spinają. A także że działają rzeczy których nie ma sensu testować jednostkow (jakieś proste wrappery, DTO, konstruktor, pierdoły, serwisy które tylko wywołują inny serwis bez logiki (aka serwisy aplikacyjne)...
Z racji tych zależności i powolności warto je wydzielić do osobnego projektu bądź kilku projektów.
Teraz co testować, jakie przypadki to zależy jak dana część jest ważna. Np system płatności będziesz chciał przetestować pewnie też integracyjnie więcej przypadków, żeby nie było wtop. Jakiś mniej ważny workflow wystarczy happy path. I może jeden unhappy.

Generalnie, przy testowaniu i w ogóle przy tworzeniu oprogramowania zależy Ci na "fast feedback loop", czyli chcesz szybko wiedzieć, że zmiana jaką zrobiłeś działa lub nie. Więc dlatego chesz oddzielić testy wolne od szybkich. np, tutaj dołoże kilka różnych testów, Tobie wystarczy jeden zestaw integracyjnych pewnie:

Kompilacja (tak, to pierwsza "bariera" do przejścia i szybka informacja zwrotna czy coś zadziała.
Unit testy
Testy integracyjne (in memory db)
Testy integracyjne z third party test serwisami - zamockowane i stabilne.
Testy integracyjne na oracle - odpalają się tylko na CI, lokalnie nie używasz w ogóle.
Testy integracyjne z third party test serwisami - niestabilne, dlatego zostały wydzielone.


Pozostało 580 znaków

2019-05-14 18:22
0

Jeżeli chodzi o nazewnictwo, to po co w ogóle rozróżniać i bić się o terminologię? test = test

edytowany 1x, ostatnio: WeiXiao, 2019-05-14 18:23
A kiwi kiwi kiwi. - somekind 2019-05-14 22:44
@somekind: przez Ciebie aż musiałem wejść na inne forum niż 4p, by dowiedzieć się, o co chodzi. ;) - Silv 2019-05-14 22:48
Zombii zombii zombii - AreQrm 2019-05-15 10:51

Pozostało 580 znaków

2019-05-14 19:32
1

Trochę z boku topic, ale w zasadzie w temacie.
ZIO - cudowne dziecko Johna De Goes - jaram się jak junior 15k.( Ale jeszcze nigdzie ZIO nie użyłem :-) )

https://scalaz.github.io/scal[...]overview/testing_effects.html

Niby nic tam nowego nie ma, testowanie pure FP tak w zasadzie wyglądało zawsze. Ale teraz może uda sie wprowadzić standard na efekty. Monada IO w Scali, ogólniejsza i chyba lepsza w praktyce od IO z haskella. Zobaczymy.


Bardzo lubie Singletony, dlatego robię po kilka instancji każdego.
edytowany 4x, ostatnio: jarekr000000, 2019-05-14 19:39

Pozostało 580 znaków

2019-05-14 19:45
0

Ale teraz może uda sie wprowadzić standard na efekty.. monada IO w Scali, ogólniejsza i chyba lepsza w praktyce od IO z haskella. Zobaczymy.

W Scali (ale też np w Javie, Kotlinie, C++, etc) jest podstawowy problem z efektami ubocznymi - kompilator w żaden sposób przed nimi nie przestrzega (nie mówiąc już o blokowaniu ich). W Haskellu nie mogę sobie przypadkowo zrobić x = x + 1 ani żadnego innego efektu ubocznego poza monadą IO. W Scali natomiast mogę przypadkowo odpalić np stringBuilder.append(5) bez opakowywania tego w monadę IO i nie będzie żadnego ostrzeżenia. Używanie monad IO w Scali wymaga dyscypliny, by nie wywołać efektu ubocznego poza przeznaczonymi do tego konstrukcjami.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
Żeby było śmieszniej... chyba wczoraj pisałem to właśnie do @Krolik a - czemu scali daleko do Haskella. - jarekr000000 2019-05-14 19:56

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0