Testy integracyjne. Co i jak?

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.

0

Nie do końca wiem w czym robisz, ale baza danych też musi być właściwa z tym że jest to inna baza, przeznaczona tylko na testy i po każdym teście wraca do stanu sprzed testu np trzymając wszystkie operacje w transakcji lub po każdym teście uruchamiając refresh na migracjach. Innymi słowy, jeśli projekt ma być używany z mysql to musisz robić testy na mysql a nie na sqllitle

13

Dawno temu miałem takie rozkminy, ale mi przeszło.
Przestałem jakoś specjalnie rozróżniać testy integracyjne.

Testy to testy:

  1. podaję warunki początkowe,
  2. zapuszczam działanie,
  3. sprawdzam wynik.

Czy po drodze (punkt 1) wypełniam jakąs hashmapę, czy 4 bazy danych to już szczegół implementacyjny.

Bywa, że ze względów praktycznych trzeba wydzielić inicjalizację bazy danych, żeby nie robić przed każdym testem (i wtedy rollbacki po teście).
Bywa, że trzeba dłuzej trwające testy jakoś oznaczyć, by ich np. nie odpalać za każdym razem u programisty.

Ale to są szczegóły.

1

Pisanie dobrych testów wynika przede wszystkim ze zrozumienia architektury.

W większości przypadków można uniknąć stub'a, czy mock'a wydzielając testowaną logikę do np. klasy statycznej.

To czy piszesz klasę z testami dla metody czy innej klasy to już zależy od twojej kreatywności. Ja lubie np. Partiale używać na inną ścieżke testów.

Ktoś tam coś mówił o słowniku. Co to znaczy mockować? :D

Na poważnie to kiedy ludzie zaczną odróżniać mock od stuba i fake.?

Nie ma nic złego w odróżnianiu testów. :P Tyle że pisanie samych testów integracyjnych, jest w większości przypadków słabe. Lepiej od razu pisać Funkcyjne/Akceptacyjne w stylu BDD z konkretnym label'em pod funkcjonalność.

1
Gworys napisał(a):

...
Na poważnie to kiedy ludzie zaczną odróżniać mock od stuba i fake.?
..

Tak, z ciekawości, gdzie jest wartość dodana w nazywaniu na różne sposoby (mock, stub, fake) implementacji użytej na potrzeby testów ?
To nie jest tak jak z tym dowcipem o juhasach i bacy, który rozwiewa wątpliwości jak nazywać jeża poprawnie?

0
yarel napisał(a):
Gworys napisał(a):

...
Na poważnie to kiedy ludzie zaczną odróżniać mock od stuba i fake.?
..

Tak, z ciekawości, gdzie jest wartość dodana w nazywaniu na różne sposoby (mock, stub, fake) implementacji użytej na potrzeby testów ?
To nie jest tak jak z tym dowcipem o juhasach i bacy, który rozwiewa wątpliwości jak nazywać jeża poprawnie?

Jeśli ktoś mówi na rower samochód, to zwykle są ku temu tylko dwa powody, ale to może być subiektywne odczucie, więc bym się tym zbytnio nie przejmował. :P

2
Gworys napisał(a):

Jeśli ktoś mówi na rower samochód, to zwykle są ku temu tylko dwa powody, ale to może być subiektywne odczucie, więc bym się tym zbytnio nie przejmował. :P

powód pierwszy:
rower.jpg

1
Gworys napisał(a):
yarel napisał(a):
Gworys napisał(a):

...
Na poważnie to kiedy ludzie zaczną odróżniać mock od stuba i fake.?
..

Tak, z ciekawości, gdzie jest wartość dodana w nazywaniu na różne sposoby (mock, stub, fake) implementacji użytej na potrzeby testów ?
To nie jest tak jak z tym dowcipem o juhasach i bacy, który rozwiewa wątpliwości jak nazywać jeża poprawnie?

Jeśli ktoś mówi na rower samochód, to zwykle są ku temu tylko dwa powody, ale to może być subiektywne odczucie, więc bym się tym zbytnio nie przejmował. :P

Cóż, różnica jest taka, że tu mamy zdefiniowany jasno kontekst (testy i implementacja na potrzeby testów), a w Twoich rozważaniach o rowerach i samochodach jakoś tego kontekstu nie wskazujesz. Obydwa są pojazdami, obydwa mogą być kupione itd.

W code review, takie rozróżnianie, u co bardziej zapalczywych jednostek, może prowadzić do patalogii typu: "nazwałeś plik FooStub, a to jest przecież FooMock" i tym samym odrywać ludzi od rozwiązywania właściwych problemów.

Gdzie, to rozróżnianie nazewnictwa implementacji w testach przynosi korzyść?

0
yarel napisał(a):
Gworys napisał(a):
yarel napisał(a):
Gworys napisał(a):

...
Na poważnie to kiedy ludzie zaczną odróżniać mock od stuba i fake.?
..

Tak, z ciekawości, gdzie jest wartość dodana w nazywaniu na różne sposoby (mock, stub, fake) implementacji użytej na potrzeby testów ?
To nie jest tak jak z tym dowcipem o juhasach i bacy, który rozwiewa wątpliwości jak nazywać jeża poprawnie?

Jeśli ktoś mówi na rower samochód, to zwykle są ku temu tylko dwa powody, ale to może być subiektywne odczucie, więc bym się tym zbytnio nie przejmował. :P

Cóż, różnica jest taka, że tu mamy zdefiniowany jasno kontekst (testy i implementacja na potrzeby testów), a w Twoich rozważaniach o rowerach i samochodach jakoś tego kontekstu nie wskazujesz. Obydwa są pojazdami, obydwa mogą być kupione itd.

W code review, takie rozróżnianie, u co bardziej zapalczywych jednostek, może prowadzić do patalogii typu: "nazwałeś plik FooStub, a to jest przecież FooMock" i tym samym odrywać ludzi od rozwiązywania właściwych problemów.

Gdzie, to rozróżnianie nazewnictwa implementacji w testach przynosi korzyść?

Ja bije do tego, że jeśli nie potrafisz, odróżnić jednego od drugiego to jest źle. To czy dopisujecie do nazwy stub, czy dummy, spy etc... To już problem spójnej konwencji, jaką przyjęliście.

Uważam również, że w komunikacji codziennej, kiedy mówimy o testach powinniśmy odróżniać stub od mock'a, ponieważ zmienia to kontekst wypowiedzi.

Gdzie, to rozróżnianie nazewnictwa implementacji w testach przynosi korzyść?

Tam gdzie czytelność testów ma znaczenie.

1
Gworys napisał(a):

...

Gdzie, to rozróżnianie nazewnictwa implementacji w testach przynosi korzyść?

Ja bije do tego, że jeśli nie potrafisz, odróżnić jednego od drugiego to jest źle. To czy dopisujecie do nazwy stub, czy dummy, spy etc... To już problem spójnej konwencji, jaką przyjęliście.

Uważam również, że w komunikacji codziennej, kiedy mówimy o testach powinniśmy odróżniać stub od mock'a, ponieważ zmienia to kontekst wypowiedzi.

Ok, rozumiem Twój punkt widzenia.

Gdzie, to rozróżnianie nazewnictwa implementacji w testach przynosi korzyść?

Tam gdzie czytelność testów ma znaczenie.

Ten argument jest dość ogólny, bo czytelność kodu jest raczej powszechnie pożądaną cechą. Brakuje mi tu jednak jakiegoś powiązania między czytelność kodu i rozróżnianiem różnych implementacji. Masz jakieś przykłady, gdzie zmiana nazewnictwa jakoś istotnie wpłynęła na czytelność testów? np. "ServiceTestImpl zamieniliśmy na ServiceMockImpl i to uporządkowało nam testy". Nie wiem, dlatego dopytuję celem aktualizacji bazy wiedzy :-)

0
yarel napisał(a):
Gworys napisał(a):

...

Gdzie, to rozróżnianie nazewnictwa implementacji w testach przynosi korzyść?

Ja bije do tego, że jeśli nie potrafisz, odróżnić jednego od drugiego to jest źle. To czy dopisujecie do nazwy stub, czy dummy, spy etc... To już problem spójnej konwencji, jaką przyjęliście.

Uważam również, że w komunikacji codziennej, kiedy mówimy o testach powinniśmy odróżniać stub od mock'a, ponieważ zmienia to kontekst wypowiedzi.

Ok, rozumiem Twój punkt widzenia.

Gdzie, to rozróżnianie nazewnictwa implementacji w testach przynosi korzyść?

Tam gdzie czytelność testów ma znaczenie.

Ten argument jest dość ogólny, bo czytelność kodu jest raczej powszechnie pożądaną cechą. Brakuje mi tu jednak jakiegoś powiązania między czytelność kodu i rozróżnianiem różnych implementacji. Masz jakieś przykłady, gdzie zmiana nazewnictwa jakoś istotnie wpłynęła na czytelność testów? np. "ServiceTestImpl zamieniliśmy na ServiceMockImpl i to uporządkowało nam testy". Nie wiem, dlatego dopytuję celem aktualizacji bazy wiedzy :-)

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

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.

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.

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.

0

Tu mam taki trochę złośliwy przykład:
https://github.com/javaFunAgain/ratpong/blob/master/src/test/java/pl/setblack/pongi/users/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.

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.

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);
    }
}
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.

0

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

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/scalaz-zio/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.

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.

3
Juhas napisał(a):

Dobra, trochę odbiegamy od tematu. Czuję się jakbym rzucił kiełbasą w stado wilków :D

Raczej dżdżownic.

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.

Nie czytałem żadnych książek o tej tematyce, więc nie znam oficjalnych definicji, posługuję się intuicyjnymi:

  • Testy akceptacyjne robi klient/BA/Product Owner albo ktoś inny odpowiadający za wymagania. To nie jest sprawdzanie, czy aplikacja działa i czy 2+2=4, tylko czy dana funkcja robi to, czego klient w ogóle chciał.
  • Testy funkcjonalne to testy uruchamiane na działającej aplikacji i sprawdzające czy działa i spełnia wymagania. Nie wymagają wiedzy o wewnętrznej implementacji, więc często są tworzone przez testerów.
  • Testy integracyjne są najtrudniejsze, bo różni ludzie nazywają tak różne rzeczy.
    a) Dla jednych to testy sprawdzające, czy klasa X działa ze swoimi zależnościami. (Bo w swoich "testach jednostkowych" zawsze mockują wszystkie zależności testowanej klasy.)
    b) Dla innych to testy sprawdzające działanie jakichś komponentów systemu z innymi komponentami systemu na żywo. Np. czy klasa przetwarzająca dane dane z jakiegoś API/bazy/pliku poprawnie przetwarza dane faktycznie pobrane z tego API/bazy/pliku, a nie z jakiegoś mocka w pamięci.
    c) Dla jeszcze innych to to samo, co testy funkcjonalne - bo de facto jedne i drugie sprawdzają zachowanie tworzonej aplikacji/serwisu/API z jego zależnościami w postaci

Moja osobista opinia jest taka, że:
a) to patologia i kompletne niezrozumienie idei testowania jednostkowego;
b) czasami ma sens (np. napisaliśmy swojego ORMa, i chcemy sprawdzić, czy dane odczytane z bazy są takie same jak te wstawione), ale często też jest oznaką, że jakiś projekt jest bardzo "enterprise" i cierpi na nadmiar warstw, komponentów i porąbanych technologii (mam takie rzeczy w pracy, np. testy integracyjne sprawdzające czy da się utworzyć obiekt bazując na konfiguracji IoC w XML albo czy jakieś klasy ze środkowej warstwy fizycznie kontaktują się z API, z którego korzystają). Nie mówię, że istnienie takich testów jest złe, po prostu ich pisanie było stratą czasu wynikającą z błędów architektury lub doboru technologii.
c) mam takie wrażenie, że w typowym nieprzekombinowanym projekcie, testy integracyjne i funkcjonalne się zrównują, bo testujemy, czy uruchomiona aplikacja działa prawidłowo i daje oczekiwane rezultaty współpracując z faktyczną bazą/plikami/zewnętrznymi usługami.

Juhas napisał(a):

Jeden plik testuje jedną klasę

Załóżmy, że to jakaś klasa utilsowa, która ma 10 metod. Załóżmy, że dla każdej metody rozpatrujemy 10 przypadków dla prawidłowych danych (typowe i brzegowe warunki) oraz 10 przypadków, kiedy metoda zwraca błąd/rzuca wyjątek. Załóżmy, że piszemy najkrótsze możliwe przypadki testowe, czyli: 3 linijki na kod testowy (arrange, act, assert), 2 linijki odstępu między poszczególnymi fazami, 3 linijki na sygnaturę metody oraz klamerki i linijkę na odstęp między metodami testowymi. Daje to razem 9 linijek na test.
10 * 20 * 9 = 1800 linijek kodu w pliku. Dla mnie to nie brzmi jak coś prostego.

Czasami może i zdarza mi się, że mam wszystkie testy jednej klasy w jednym pliku. Ale to chyba tylko wtedy, gdy klasą jest jakiś handler/serwis z jedną metodą publiczną i prawie nie zawiera logiki. Znacznie częściej do jednej testowanej metody mam wiele plików: oddzielne na błędy walidacji wejście, oddzielne na wyjątki rzucane podczas przetwarzania, oddzielne pozytywny przepływ - a i to nieraz dzielę, jeśli mam jakiś setup wspólny dla kilku przypadków testowych, gdy inne przypadki wymagają innego setupu.

Jeśli chodzi o testy integracyjne, to czytam różne rzeczy. Że tu testujemy np. tylko happy path. Czy to jest reguła?

Nie wiem, czy to reguła, ale dla mnie testy wyłącznie happy path to de facto brak testów. Bo nie da się testami udowodnić, że kod zadziała w każdym przypadku, ale musisz pokazać, w jakich przypadkach zgodnie z oczekiwaniami nie zadziała.

Ja do wszystkiego używam nUnit.

Ja również. Ale w przypadku testów integracyjnych "po mojemu" pisanych do API webowego nie ma problemu, aby użyć w ogóle innej technologii. W końcu request HTTP można podobno wysłać nawet w JS.

yarel napisał(a):

Tak, z ciekawości, gdzie jest wartość dodana w nazywaniu na różne sposoby (mock, stub, fake) implementacji użytej na potrzeby testów ?
To nie jest tak jak z tym dowcipem o juhasach i bacy, który rozwiewa wątpliwości jak nazywać jeża poprawnie?

Myślę, że chodzi o wyrywanie lachonów z dziwnymi fetyszami. ;)

A tak na serio, to nie sądzę aby ktoś Ci odpowiedział na to pytanie. Bo odpowiedzieć mogą tylko ludzie, którzy faktycznie piszą tego typu klasy, czyli jaskiniowcy, a oni jeszcze nie wynaleźli języka.

Z pragmatycznego punktu widzenia prościej stworzyć SUT z automatycznymi mockami wszystkich ulotnych zależności oraz automatycznym stubowaniem zwracanych danych. Jak trzeba, to się po prostu stubuje po swojemu, to co się chce i można weryfikować wywołania metod. I nie trzeba się męczyć z dyskusjami na temat tego, czy prawidłowo się nazwało fejkowe klasy w projektach testowych. No chyba, że ktoś chce mieć wąską specjalizację, zostać senior test double developerem i debatować w nieskończoność, czy mock jest spy'em czy odwrotnie.

0

Z pragmatycznego punktu widzenia prościej stworzyć SUT z automatycznymi mockami wszystkich ulotnych zależności oraz automatycznym stubowaniem zwracanych danych.

A dlaczego nie CUT, OUT, Substitute.? Czy ten SUT to nie jest też jakieś zboczenie.?

Jeśli do stubowania jednego parametru potrzebujesz frameworka izolacji to prawdopodobnie sam się nabawiłeś jakiegoś dziwnego fetyszy od kolegów z pracy. :D

Testy akceptacyjne robi klient/BA/Product Owner albo ktoś inny odpowiadający za wymagania. To nie jest sprawdzanie, czy aplikacja działa i czy 2+2=4, tylko czy dana funkcja robi to, czego klient w ogóle chciał.

A jak sprawdzić, czy dana funkcja robi to, co chce klient, jeśli aplikacja nie działa, czyli nie dodaje 2+2? Kto ci takich bzdur nagadał.?
Testy Akceptacyjne mają gwarantować akceptacje oprogramowania przez klienta. Może to tylko u was klienci akceptują aplikacje, które nie działają.
Testy akceptacyjne nie robi klient.
Testy akceptacyjne wykonuje się z klientem/Product Owner'em.

0
Gworys napisał(a):

A dlaczego nie CUT, OUT, Substitute.? Czy ten SUT to nie jest też jakieś zboczenie.?

Użyłem nazwy, która jest zrozumiała dla wszystkich.
No, najwyraźniej prawie dla wszystkich.

Jeśli do stubowania jednego parametru potrzebujesz frameworka izolacji to prawdopodobnie sam się nabawiłeś jakiegoś dziwnego fetyszy od kolegów z pracy. :D

Nie potrzebuję stubować jednego parametru, nie mam takich malutkich projektów.

A jak sprawdzić, czy dana funkcja robi to, co chce klient, jeśli aplikacja nie działa, czyli nie dodaje 2+2? Kto ci takich bzdur nagadał.?

Napisałem, co jest celem testów akceptacyjnych - sprawdzanie zgodności z wymaganiami, a nie samo działanie, bo to jest weryfikowane innymi testami znacznie wcześniej.
To, że nie umiesz czytać ze zrozumieniem to już nie mój problem.

0

Użyłem nazwy, która jest zrozumiała dla wszystkich.
No, najwyraźniej prawie dla wszystkich.

Jak to dobrze wypowiadać się za wszystkich. :D

Nie potrzebuję stubować jednego parametru, nie mam takich malutkich projektów.

A co mnie obchodzą twoje projekty.? Masz jakąś manie wielkości.? :D Rozumiem, że nie uświadczysz w owych projektach obiektów z metodami, które mają 1 parametr, super :D

Napisałem, co jest celem testów akceptacyjnych - sprawdzanie zgodności z wymaganiami, a nie samo działanie, bo to jest weryfikowane innymi testami znacznie wcześniej.
To, że nie umiesz czytać ze zrozumieniem to już nie mój problem.

To dlaczego te testy nie nazywają się testy zgodności wymagań.? HAHA :D

0
Gworys napisał(a):

A co mnie obchodzą twoje projekty.?

Skoro cię nie obchodzą, to po co pytasz? Jak dla mnie EOT.

@Silv - jeśli masz jakieś konkretne pytania, to po prostu pytaj.

4

Bez sensu wymyślać kilkanaście nic nieznaczących nazw, łatwiej podzielić po czasie i wyśiłku wymaganego do zrobienia testu:
-takie które odpala się z IDE i trwają kilka sekund (odpalane przy każdej zmianie)
-takie które trwają dłużej, ale fajnie je mieć bo działają na żywym organizmie (zazwyczaj odpalane raz przed merge)
...i tyle.
Resztę testów mnie jako deva nie obchodzi bo to nie moja odpowiedzialność.

A co do tego czy coś jest fake, stubem czy innym wynalazkiem. Nazwij to po prostu InMemoryCostam i problem z głowy

4

@danek

a o czym później ludzie będą opowiadać na konferencjach i pisać na blogach?

4

Teza o szybkości testów jest słaba. Nieraz jak testuję np jakiegoś aktora Akkowego to test może trwać nawet kilka sekund, bo takie sobie ustawiam timeouty. Operacje na aktorach dzieją się asynchronicznie, a test działa synchronicznie, więc trzeba robić awaity z timeoutami. Z drugiej strony mogę mieć test, który tworzy pliki na dysku, ale działa 10x szybciej. Co z tego wynika? To, że test jednostkowy niekoniecznie musi być szybki. Może być cholernie wolny, ale jeśli zysk z niego jest wystarczająco duży, to co z tego? Mogę sobie pogrupować testy (otagować) i jedną komendą odpalać tylko szybkie testy, a drugą komendą odpalać wszystkie testy. Jak kto woli.

Zamiast się głowić jak kategoryzować testy albo powstrzymywać się przed stworzeniem tymczasowego pliku w teście jednostkowym w obawie, że ktoś się wścieknie wolę skupić się na praktycznych aspektach testów, a więc kategoryzuję n.p. tak:

  • samodzielne testy, które nie wymagają żadnych działających środowisk (tzn uprzedniego stawiania i konfigurowania aplikacji na jakichś maszynach), ale wystarczy jest odpalić prostym skryptem i działają na każdym kompie z poprawnie skonfigurowanym build toolem
  • testy wymagające zewnętrznych środowisk - takie testy mogą być poprawnościowe lub wydajnościowe

To jaką ktoś przyczepi etykietkę do testu to sprawa drugorzędna lub trzeciorzędna. I tak każdy ma inne intuicje i okazuje się, że termin np testy akceptacyjne jest przez każdego inaczej rozumiany. Zamiast tego lepiej dogadać się jakie katergorie testów są ważne w konkretnym projekcie. Dla przykładu w firmie mamy taką sytuację:

  • mamy wiele mikroserwisów i każdy ma builda w TeamCity
  • używamy więc często terminologii "testy serwisu A", "testy serwisu B", etc
  • zwykle jakiego rodzaju są testy w środku danego serwisu nie ma specjalnie znaczenia o ile te testy nie łączą się poza maszynę która je odpala ani nie wymagają specjalnego przygotowania maszyny (np stawianie apek)
  • jednak jest pewien wyjątek - mamy specjalny framework do tworzenia testów (nazywa się BackGarden) i operuje on na globalnym mutowalnym stanie, klika poprzez Selenium, stawia kilka JVMek, komunikuje się Akką między JVMkami i robi jeszcze inne dziwne rzeczy - te testy często padają i trzeba puszczać builda w kółko, więc wystarczy powiedzieć, że padają nam testy BackGardenowe i wszyscy wiedzą, że zadanie jest prawie gotowe, ale trzeba upolować zielonego builda
  • sporo testów jest w buildach, które nie dotyczą żadnego mikroserwisu, tzn build zawiera tylko kod testowy, gdzie testy są poprawnościowe czy wydajnościowe
  • pewne testy (np testy end-to-end) łączą się do np środowiska UAT, więc jeśli padają to zwykle oznacza iż z tym środowiskiem jest coś nie teges (zła konfiguracja mikroserwisu, wypięcie się mikroserwisów z Kafki, itd). Jeśli natomiast działają to dają większą pewność, że system działa na świeżych realnych danych niż testy ze sztywnymi danymi (tzn te w kodzie mikroserwisów), które ktoś kiedyś wrzucił do repozytorium VCSs.
  • część testów wymaga łączenia się z zewnętrznymi serwisami (np spoza naszej firmy w ogóle), które w pewnych porach nie działają, np w środę przez parę godzin pewne testy nam nie mają prawa iść, więc je w tym czasie ignorujemy

Powyższa kategoryzacja jest konkretna i ma znaczenie zarówno dla programistów jak i menedżera, biznesu, a także zewnętrznych zespołów. Natomiast to czy ja sobie w jakimś teście, który do tej pory był 100% unitowy (według wszelkich wyrytych w skałach zasad) zacznę tworzyć tymczasowy plik na dysku to jest taka pierdoła, że nie warto się nad tym pochylać. Jeśli stworzenie tymczasowego pliku umożliwi stworzenie sensownego testu (o wciąż akceptowalnej wydajności), gdzie lepiej pokrywam rzeczywisty kod biznesowy, a nie robię kolejnego testu Mockito to jak najbardziej warto taki tymczasowy plik stworzyć i zapomnieć o mockach przy testach danej klasy.

0
danek napisał(a):

Bez sensu wymyślać kilkanaście nic nieznaczących nazw, łatwiej podzielić po czasie i wyśiłku wymaganego do zrobienia testu:
-takie które odpala się z IDE i trwają kilka sekund (odpalane przy każdej zmianie)
-takie które trwają dłużej, ale fajnie je mieć bo działają na żywym organizmie (zazwyczaj odpalane raz przed merge)
...i tyle.
Resztę testów mnie jako deva nie obchodzi bo to nie moja odpowiedzialność.

A co do tego czy coś jest fake, stubem czy innym wynalazkiem. Nazwij to po prostu InMemoryCostam i problem z głowy

Moim zdaniem wy po prostu nie rozumiecie różnicy pomiędzy jednym a drugim. Więc najłatwiej jest to po prostu wyśmiać lub stanąć po stronie jakiegoś urojonego lidera.

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