Czy testy integracyjne powinny obejmować te same przypadki testowe co testy jednostkowe?

0

Problem pojawił się właściwie podczas pisania testów integracyjnych do mojej implementacji API do TSI.

Czytałem już to -> https://softwareengineering.stackexchange.com/questions/321399/are-integration-tests-meant-to-repeat-all-unit-tests, ale niewiele widzę tam wskazówek co do mojego problemu.

Nie chodzi mi o to, jakie są granice testów integracyjnych, a jakie testów jednostkowych. O co mi chodzi – najlepiej na przykładzie.

Mam skrypt (plik) o nazwie tsi-bash-util. Wywołując go, mogę m.in. tworzyć "dokumenty". Tworzenie dokumentów jest obsługiwane przez następującą składnię: tsi-bash-util create doc arg1 arg2 arg3 …. Argumentów argX może być dowolna liczba, ale nie może być ich zero. Gdy jest zero, wykonywanie skryptu jest przerywane i zwracany jest status reprezentujący błąd. Tworzenie dokumentów w kodzie jest wykonywane funkcją tsi_create_doc – to znaczy, wywołując tsi-bash-util create doc arg1 arg2 arg3 … wywołuję tym samym tsi_create_doc arg1 arg2 arg3 …. Tak więc jeśli testy jednostkowe tej funkcji obejmują już sprawdzanie, czy liczba argumentów jest zero, to czy powinienem sprawdzać także ten przypadek w testach integracyjnych – tych, które obejmują testowanie składni tsi-bash-util create doc?

Jak coś niejasne, pytajcie.


PS Dlaczego o to pytam: logicznym dla mnie jest (czy poprawnie?), że zarówno na poziomie testowania funkcji chcę wiedzieć, że dla zera argumentów funkcja zwróci odpowiedni status, jak i na poziomie skryptu chcę wiedzieć, że skrypt zwróci odpowiedni status dla zera argumentów. Mógłbym testować w zasadzie tylko na poziomie skryptu; ale w ten sposób nie wiedziałbym, czy dana funkcja działa zgodnie z oczekiwaniem. Może tak powinno tu właśnie być – że funkcje nie są ważne, tylko sam skrypt? W takim razie kiedy testowanie zarówno funkcji, jak i skryptu miałoby sens?

10

Nie ma potrzeby powtarzania.

Testy na poziomie wyższym (integracyjne) nie muszą sprawdzać po kolei wszystkich przypadków na poziomie niższym.
Jeśli masz dobrze przetestowane komponenty składowe to na poziomie wyższym testujesz tylko czy dobrze je posklejałeś - czasem jeden test wystarczy (zależy jak bardzo to *posklejanie *jest skomplikowane).
To jest standardowo nawet obrazowane przez tzw. piramidę testów - im wyżej tym mniej.

Jeśli pracujesz w jakimś solidnym języku programowania (powiedzmy Haskell lub funkcyjna Scala) to w często nie ma nawet co testować "integracyjnie" - jeśli się skompilowało to znaczy, że musi działać.

Patrząc z drugiej strony - jeśli już masz testy integracyjne to unitowe nie są konieczne. Jeśli chcesz rozwijać program to przeważnie warto je mieć, bo ułatwią Ci znajdowanie błędów (skrócą czas). Ale z punktu widzenia zapewnienia jakości produktu nie są konieczne. Jeśli masz 100% ścieżek i przypadków brzegowych pokryte "integracyjnie" - to wystarczy. Możesz sobie robić deploy w piątek o 15:55 i jechać na wakacje.

(Dawno temu uprawiałem londyńskie TDD == overmocking i wtedy nawet robiłem zamiatanie testów unitowych - po zrobieniu integracji wywalałem tak 50-75% testów unitowych, bo tylko przeszkadzały. Dopiero po jakimś czasie odkryłem, że po prostu robię debilne testy i zamiast je wyrzucać - lepiej po prostu zmienić sposób pisania.)

Osobnym problem (równoległym) są tzw. testy akceptacyjne.
Jeśli robisz narzędzie (command line) - i masz opis jak działa (przykłady) to "dla spokoju" warto sobie te przykłady wrzucić w testy.
Jeśli masz zdefiniowany przez klienta zestaw funkcji i warunków jakie program ma spełniać - to warto sobie dokładnie te przypadki wrzucić jako testowe.

10

Jak na moje tak. Doskonałe przykłady tego są na różnych memach jak ten:

Testy jednostkowe przejdą, ale już w spięciu z innymi komponentami mogą doprowadzić do zmian, które spowodują błędne zachowanie aplikacji.

8

Wg mnie testy integracyjne muszą być, a jednostkowe mogą/powinny być. Działające testy integracyjne == działająca aplikacja. Unit testy piszę wtedy, gdy jest spełniony co najmniej jeden z warunków:

  • dany unit można w sposób banalny przetestować (np. funkcja sprawdzająca poprawność adresu email - zazwyczaj nie ma żadnych zależności, jest pure),
  • unit obsługuje wiele edge case'ów,
  • sklejenie wszystkich scenariuszy (requestów, komend, whatever) w testach integracyjnych, które pozwolą przetestować jakiś unit i wszystkie jego rozgałęzienia jest jak kopanie się z koniem,
  • mam za dużo czasu.

Tak więc odpowiadając na twoje pytanie: jeśli jesteś w stanie pokryć testami integracyjnymi do tsi-bash-util create doc 100% kodu zawartego w tsi_create_doc w prosty sposób, olej unit testy. Jeśli natomiast w tsi_create_doc jest dużo logiki, np. nazwy parametrów nie mogą się powtarzać, nazwa parametru nie może być taka sama jak nazwa istniejącego pliku w obecnym katalogu, czy parametry muszą być posortowanie alfabetycznie ¯\_(ツ)_/¯ to napisałbym testy integracyjne do happy path i/lub przykładów z dokumentacji dla tsi-bash-util create doc, a do tsi_create_doc napisałbym unit testy.

5

Nie widzę jak chcesz pokrywać "te same" przypadki testami. Testy integracyjne generalnie są na poziomie API twojej aplikacji i są blackboxowe -> wywołujesz API aplikacji i sprawdzasz czy wynik zgadza się z oczekiwaniami. Testy jednostkowe zwykle są na dużo niższym poziomie i sprawdzają np. czy przechodzisz przez odpowiednie ścieżki w kodzie.
Moim zdaniem należy zacząć od pokrycia testami integracyjnymi a unity pisać tam gdzie faktycznie mamy jakąś logikę którą chcemy szczegółowo sprawdzić. W praktyce takich miejsc jest niewiele ;)
Pamiętaj też ze API aplikacji zmienia się powoli, a wewnętrzne interfejsy już niekoniecznie. W efekcie test który sprawdza ze tsi-bash-util create woła pod spodem funkcje tsi-create-doc posypie się jak tylko podzielisz tą metodę na 2 inne, podczas gdy test integracyjny będzie działać jak złoto.

2

Polegając głównie na integracyjncych testach doprowadzasz do podziału jak z tym lodem:

Z czasem testy integracyjne będą coraz trudniejsze w napisaniu, będą mniej jasne, będą dłużej się wykonywać. Testy same w sobie nie są dowodem na działanie oprogramowania, są swego rodzaju sitem do wyłapania błędów, a testy integracyjne to dość słabe sito, także otrzymasz główne powierzchowne błędy.

W tym celu, by pogłębić liczbę przypadków, i ułatwić sobie pisanie testów stosuje się unity. Unity też mają sens jeśli chcesz poprawić swoje tempo pracy, tzn uruchamiać testy często w czasie pisania, bo unity szybciej się wykonują i w ten sposób masz lepszy feedback. Tu polecam książkę: "Praca z zastanym kodem".

Ja nie testuje wszystkiego unitowo, nie testuje kodu, który uderza do bazy. Zamiast tego dąże do tego by dzielić przetwarzanie na podejmowanie decyzji(kody typowo funkcyjne) i na wykonanie (kody typowo imperatywne). I tutaj podejmowanie decyzji testuje unitowo, a resztę w oparciu o integracyjne. Zobacz sobie przy okazji prezentacje: "functional core, imperative shell".

7

@pan_krewetek mitologia.

Z czasem testy integracyjne będą coraz trudniejsze w napisaniu, będą mniej jasne, będą dłużej się wykonywać

Jak zrobisz sensowny DSL do testów to ich setup jest dość prosty. Czas wykonania to klasyczna mitologia ludzi którzy nigdy takich testów nie napisali. Zresztą w przypadku mikro-serwisów realnie nie powinno tych testów być tak znowu dużo, z definicji. W przypadku monolitu faktycznie może to być trochę większy problem.

Testy same w sobie nie są dowodem na działanie oprogramowania

Testy jednostkowe chciałeś powiedzieć, one faktycznie niczego nie dowodzą. Ale testy integracyjne tak samo jak e2e generalnie sprawdzają dokładnie to czy oprogramowanie działa. Bo czymże jest działanie jak nie właśnie tym, że dla parametrów X system zwraca wynik Y?

W tym celu, by pogłębić liczbę przypadków, i ułatwić sobie pisanie testów stosuje się unity. Unity też mają sens jeśli chcesz poprawić swoje tempo pracy, tzn uruchamiać testy często w czasie pisania, bo unity szybciej się wykonują i w ten sposób masz lepszy feedback.

poprawić tempo - już to widzę, przy unitach które non stop trzeba poprawiać bo każda zmiana kodu je "psuje", bo nagle nie ma już klasy X albo metody Y. I znów ta mitologia z "szybkością wykonania". Patrze teraz na projekt który mam pod ręką i dowolny z testów integracyjnych wykonuje się 50-200ms. Co więcej w przeciwieństwie do unitów, wiem że jak któryś z tych testów sfailuje to znaczy że system przestał działać i jednocześnie że jeśli są zielone to mam pewność że funkcje systemu nadal działają.

Jak wywali ci się unit test to w teorii wiesz dokładnie gdzie coś poszło nie tak, tylko że w 99% przypadków to nie jest żaden bug ani popsuta funkcja systemu, tylko wynik refaktoringu.

Też kiedyś wierzyłem w 100% pokrycia testami i klepałem namiętnie unit testy do wszystkiego. A potem odkryłem że niczego mi te testy nie dają, oprócz dodatkowej roboty przy ich ciąglym poprawianiu w miarę zmieniania się kodu. Dla porównania mam testy integracyjne w kilku serwisach które nie zmieniły się od wielu miesięcy, niektóre nawet lat, mimo że implementacja zmieniła się wielokrotnie. Czemu? Bo wymagania się nie zmieniły więc system nadal powinien "zachowywać" się dokładnie tak jak się zachowywał.

Nie twierdzę żeby takich testów nie pisać wcale, bo to też bez sensu. Jak mam złożoną logikę w której chce konkretnie przetestować detale implementacyjne to napisze do niej unit test. Jak małym wycinkiem mojego systemu jest np. jakiś parser, to zrobie kupę unitów do tegoż parsera, zeby sprawdzić czy działa tak jak powinien i nie ma sensu przepychać requestów przez cały system w tym celu.

edit: żeby nie być gołosłownym

screenshot-20210309113223.png
screenshot-20210309113238.png

Z tego co widzę najdłuższy który się tu załapał ma 300ms (większe wartości to zbiorowe podsumowania dla całych zestawów) - jeśli kogoś realnie "spowalnia" w pracy test wykonujący się 300ms to chylę czoła :)

Dla jasności: mówiąc testy integracyjne nie mam na myśli testów e2e tylko testy na poziomie jednego serwisu otoczonego bazami in-memory, httpmockami itd podobnie jak w https://github.com/Pharisaeus/almost-s3

0

Ale testy integracyjne tak samo jak e2e generalnie sprawdzają dokładnie to czy oprogramowanie działa - to nadal nie jest dowód, a tylko sprawdzenie poszczególnych przypadków.

Jak zrobisz sensowny DSL do testów to ich setup jest dość prosty - i nadal powolny. Wystarczająco znośny jak testujesz pobieżnie 2-3 przypadki na funkcjonalność, ale gdy dochodzi do 20-30 przypadkow na funkcjonalność to takie podejście traci sens.

Problem osób piszących unittesty polega na tym, że oni wiążą kontekst wykonania z konteksem decyzyjnym i z tego powodu dużo mockują mimo, że samo wykonanie efektów ubocznych za bardzo ich nie interesuje,

2
Silv napisał(a):

PS Dlaczego o to pytam: logicznym dla mnie jest (czy poprawnie?), że zarówno na poziomie testowania funkcji chcę wiedzieć, że dla zera argumentów funkcja zwróci odpowiedni status, jak i na poziomie skryptu chcę wiedzieć, że skrypt zwróci odpowiedni status dla zera argumentów. Mógłbym testować w zasadzie tylko na poziomie skryptu; ale w ten sposób nie wiedziałbym, czy dana funkcja działa zgodnie z oczekiwaniem. Może tak powinno tu właśnie być – że funkcje nie są ważne, tylko sam skrypt? W takim razie kiedy testowanie zarówno funkcji, jak i skryptu miałoby sens?

No tutaj masz jakiś specyficzny przypadek, skoro Ci się to tak pokrywa. Natomiast w ogólności, to raczej pojedyncza jednostka (czyli ta Twoja funkcja) ma więcej możliwych kombinacji parametrów wejściowych, więc warto je wszystkie przetestować, aby dla każdego błędnego wejścia zwracany był błąd, a dla przykładowego prawidłowego wejścia funkcja zwracała prawidłowy wynik.
Testy integracyjne zaś mogą sprawdzić już tylko np. jeden prawidłowy przepływ oraz jeden błędny.

pan_krewetek napisał(a):

Polegając głównie na integracyjncych testach doprowadzasz do podziału jak z tym lodem:

W przypadku loda problemem jest masa testów ręcznych, o których tutaj w ogóle nie ma mowy.

Z czasem testy integracyjne będą coraz trudniejsze w napisaniu, będą mniej jasne, będą dłużej się wykonywać.

Trudność napisania testu zależy od trudności przypadku biznesowego, a to, czy testy stają się mniej jasne zależy od tego jak się je pisze.
Testy jednostkowe też stają się coraz trudniejsze w napisaniu i mniej jasne, jeśli się nie dba o ich jakość.

Shalom napisał(a):

Dla jasności: mówiąc testy integracyjne nie mam na myśli testów e2e tylko testy na poziomie jednego serwisu otoczonego bazami in-memory, httpmockami itd podobnie jak w https://github.com/Pharisaeus/almost-s3

Pytanie, czy test, który nie testuje integracji z rzeczywistym środowiskiem to wciąż test integracyjny, bo jak dla mnie to wciąż testy jednostkowe, z mockami przesuniętymi poza obręb systemu.

0

Zaznaczę jeszcze, że jestem w trakcie pisania testów integracyjnych i tego nie przerywam. Póki co, jeszcze zanim przemyślę dokładnie Wasze odpowiedzi, staram się działać intuicyjnie; głównie oznacza to powielanie przypadków testowych.

Ogólnie jeśli chodzi o takie powielanie, to wychodzi mi to dla każdego polecenia – bo założenie jest takie, że każdemu poleceniu odpowiada jedna funkcja. Problem w tym, że funkcje w zasadzie powielają API poleceń – mają podobne nazwy i ten sam zestaw parametrów. Dla przykładu:

tsi-bash-util # Obsługiwane przez skrypt główny + funkcję tsi_handle_command
tsi-bash-util create # Obsługiwane przez funkcję tsi_create
tsi-bash-util create doc # Obsługiwane przez funkcję tsi_create_doc
tsi-bash-util create tree # Obsługiwane przez funkcję tsi_create_tree
...
tsi-bash-util get attr # Obsługiwane przez funkcję tsi_get_attr
...

Zaczynam się zastanawiać, czy to nie testy są problemem, a architektura…

Ale zobaczmy na razie testy:

jarekr000000 napisał(a):

Jeśli masz dobrze przetestowane komponenty składowe to na poziomie wyższym testujesz tylko czy dobrze je posklejałeś - czasem jeden test wystarczy (zależy jak bardzo to *posklejanie *jest skomplikowane).

Tak, czytałem o tym gdzieś. Na razie jeszcze nie próbowałem wyodrębniać tych "miejsc sklejenia"; spróbuję, zobaczymy.

To jest standardowo nawet obrazowane przez tzw. piramidę testów - im wyżej tym mniej.

Chyba że, jak zauważył @somekind poniżej, jest to jakiś specyficzny przypadek i testów musi być np. tyle samo. U mnie chyba tak jest – zobacz moją odpowiedź do @somekind poniżej.

Jeśli pracujesz w jakimś solidnym języku programowania (powiedzmy Haskell lub funkcyjna Scala) to w często nie ma nawet co testować "integracyjnie" - jeśli się skompilowało to znaczy, że musi działać.

Hm. Coś w tym jest. Acz kłóciłbym się, że z założenia testy sprawdzają logikę (bez znaczenia, jaki rodzaj tych testów), a udana kompilacja nie musi oznaczać oczekiwanej logiki.

Patrząc z drugiej strony - jeśli już masz testy integracyjne to unitowe nie są konieczne. Jeśli chcesz rozwijać program to przeważnie warto je mieć, bo ułatwią Ci znajdowanie błędów (skrócą czas). Ale z punktu widzenia zapewnienia jakości produktu nie są konieczne. Jeśli masz 100% ścieżek i przypadków brzegowych pokryte "integracyjnie" - to wystarczy. Możesz sobie robić deploy w piątek o 15:55 i jechać na wakacje.

Logicznie tak by mi się też wydawało. Muszę jeszcze pomyśleć, bo do tej pory za "fundament" testowania bardziej uważałem testy jednostkowe, a testy integracyjne bardziej za "wykończenie".

Osobnym problem (równoległym) są tzw. testy akceptacyjne.
Jeśli robisz narzędzie (command line) - i masz opis jak działa (przykłady) to "dla spokoju" warto sobie te przykłady wrzucić w testy.
Jeśli masz zdefiniowany przez klienta zestaw funkcji i warunków jakie program ma spełniać - to warto sobie dokładnie te przypadki wrzucić jako testowe.

Gdzieś czytałem, że testami integracyjnymi były nazwane testy wszystkich rodzajów poza jednostkowymi. Ponieważ czytam również czasem, że jest więcej niż jedna definicja testów integracyjnych, to podoba mi się takie uogólniające podejście. I do tej pory w zasadzie je stosuję w tym programie (trochę bezwiednie): mam logicznie i strukturalnie wyodrębione jedynie dwa rodzaje testów: integracyjne i jednostkowe. Za wspomniane przez Ciebie testy akceptacyjne uważam część integracyjnych.

hauleth napisał(a):

Jak na moje tak. Doskonałe przykłady tego są na różnych memach jak ten:

Testy jednostkowe przejdą, ale już w spięciu z innymi komponentami mogą doprowadzić do zmian, które spowodują błędne zachowanie aplikacji.

Wiem, o co Ci chodzi, tylko nie widzę w tym rozwiązania problemu, raczej jego unaocznienie. Inaczej mówiąc: zdaję sobie sprawę z tej zależności, ale nadal mam problem.

iksde napisał(a):

Tak więc odpowiadając na twoje pytanie: jeśli jesteś w stanie pokryć testami integracyjnymi do tsi-bash-util create doc 100% kodu zawartego w tsi_create_doc w prosty sposób, olej unit testy.

Jestem w stanie; założenie jest takie, że, jak wyżej napisałem, każdemu poleceniu w wywołaniu programu tsi-bash-util (czyli API, z którego ma korzystać użytkownik) odpowiada jedna funkcja (czyli jedna "jednostka" w rozumieniu testów jednostkowych). To jest jakby połączenie najwyższego poziomu abstrakcji z najniższym. Jednocześnie jest to jedyna "integracja" (w rozumieniu testów integracyjnych), jaką udało mi się prawie bez wysiłku wyodrębnić w kodzie Basha. Na próby wyodrębnienia większej liczby poziomów abstrakcji przy obecnej architekturze jakoś nie mam ochoty…

Jeśli natomiast w tsi_create_doc jest dużo logiki, np. nazwy parametrów nie mogą się powtarzać, nazwa parametru nie może być taka sama jak nazwa istniejącego pliku w obecnym katalogu, czy parametry muszą być posortowanie alfabetycznie ¯\_(ツ)_/¯ to napisałbym testy integracyjne do happy path i/lub przykładów z dokumentacji dla tsi-bash-util create doc, a do tsi_create_doc napisałbym unit testy.

Wiesz co, myślałem nad tym, by testami jednostkowymi objąć jedynie wszystkie unhappy paths, a testami integracyjnymi jedynie happy path. No ale wydało mi się to dziwne; chcę w końcu też wiedzieć, czy wywołując dane API, użytkownik dostanie błąd, czy nie (część błędów obsługiwana jest przez dwie funkcje – tę, w której nastąpił błąd, i funkcję-handler danego błędu).

Shalom napisał(a):

Nie widzę jak chcesz pokrywać "te same" przypadki testami.

Ciekawe spostrzeżenie. Czy chodzi Ci o to, że testując np. z argumentem X, to testowanie, czy poleci wyjątek dla tego argumentu, na poziomie funkcji oznacza co innego niż testowanie tego na poziomie API całej aplikacji?

Pamiętaj też ze API aplikacji zmienia się powoli, a wewnętrzne interfejsy już niekoniecznie. W efekcie test który sprawdza ze tsi-bash-util create woła pod spodem funkcje tsi-create-doc posypie się jak tylko podzielisz tą metodę na 2 inne, podczas gdy test integracyjny będzie działać jak złoto.

Coś w tym jest.

pan_krewetek napisał(a):

Z czasem testy integracyjne będą coraz trudniejsze w napisaniu, będą mniej jasne, będą dłużej się wykonywać. Testy same w sobie nie są dowodem na działanie oprogramowania, są swego rodzaju sitem do wyłapania błędów, a testy integracyjne to dość słabe sito, także otrzymasz główne powierzchowne błędy.

Rzeczywiście, zgodziłbym się, że jeśli nie zdefiniuje się dokładnie pojęcia "oczekiwanego/poprawnego działania", to stwierdzenie, że program "działa poprawnie", nic nie znaczy – niezależnie od tego, czy są testy, czy nie.

Ja nie testuje wszystkiego unitowo, nie testuje kodu, który uderza do bazy. Zamiast tego dąże do tego by dzielić przetwarzanie na podejmowanie decyzji(kody typowo funkcyjne) i na wykonanie (kody typowo imperatywne). I tutaj podejmowanie decyzji testuje unitowo, a resztę w oparciu o integracyjne. Zobacz sobie przy okazji prezentacje: "functional core, imperative shell".

Nie znałem takiego podziału. Ciekawe.

Shalom napisał(a):

@pan_krewetek mitologia.

Testy same w sobie nie są dowodem na działanie oprogramowania

Testy jednostkowe chciałeś powiedzieć, one faktycznie niczego nie dowodzą. Ale testy integracyjne tak samo jak e2e generalnie sprawdzają dokładnie to czy oprogramowanie działa. Bo czymże jest działanie jak nie właśnie tym, że dla parametrów X system zwraca wynik Y?

@Shalom, zobacz to, co wyżej napisałem do @pan_krewetek

pan_krewetek napisał(a):

Problem osób piszących unittesty polega na tym, że oni wiążą kontekst wykonania z konteksem decyzyjnym i z tego powodu dużo mockują mimo, że samo wykonanie efektów ubocznych za bardzo ich nie interesuje,

Ja na przykład, mówiąc mimochodem, niczego nie mockuję. Uznałem to za karkołomne przy mojej architekturze. Może to źle?

somekind napisał(a):
Silv napisał(a):

PS Dlaczego o to pytam: logicznym dla mnie jest (czy poprawnie?), że zarówno na poziomie testowania funkcji chcę wiedzieć, że dla zera argumentów funkcja zwróci odpowiedni status, jak i na poziomie skryptu chcę wiedzieć, że skrypt zwróci odpowiedni status dla zera argumentów. Mógłbym testować w zasadzie tylko na poziomie skryptu; ale w ten sposób nie wiedziałbym, czy dana funkcja działa zgodnie z oczekiwaniem. Może tak powinno tu właśnie być – że funkcje nie są ważne, tylko sam skrypt? W takim razie kiedy testowanie zarówno funkcji, jak i skryptu miałoby sens?

No tutaj masz jakiś specyficzny przypadek, skoro Ci się to tak pokrywa. Natomiast w ogólności, to raczej pojedyncza jednostka (czyli ta Twoja funkcja) ma więcej możliwych kombinacji parametrów wejściowych, więc warto je wszystkie przetestować, aby dla każdego błędnego wejścia zwracany był błąd, a dla przykładowego prawidłowego wejścia funkcja zwracała prawidłowy wynik.

@somekind , zobacz to, co napisałem na początku tego postu. Być może cała ta aplikacja jest "specyficznym przypadkiem", ale tutaj wszystkie wywołania i odpowiadające im funkcje są podobne do tego wywołania, które opisałem w pierwszym poście. Nie jestem pewien, czy powinienem zmienić architekturę (acz nie widzę zysku w zmianie), czy testy. Najniższy poziom abstrakcji to funkcje, najwyższy – API dla użytkownika. Osbługę błędów starałem się uprościć możliwie najbardziej (z uwagi na to, że to Bash). Co chyba tu istotne, każdy błąd skutkuje po prostu przerwaniem programu; przepływ sterowania dla błędu idzie tak: użytkownik -> moje API -> funkcja -> błąd -> funkcja obsługi błędu -> użytkownik.

Testy integracyjne zaś mogą sprawdzić już tylko np. jeden prawidłowy przepływ oraz jeden błędny.

Napisałem o tym wyżej do @iksde

3

OK, to, co mówisz, ma sens, ale nie rozumiem, jak to się odnosi do mojej uwagi? Chodziło mi o to, byś zwrócił uwagę na ten fragment:...

ok no to znowu przykład: Załóżmy że piszemy serwis który pozwala ściągać jakieś pliki. Mamy w wymaganiach / use case takie rzeczy:

  • User musi być zalogowany żeby korzystać, inaczej 401
  • User z rolą X albo rolą Y może ściągać pliki, inaczej dostaje 403 jeśli nie ma takiej roli
  • Jeśli pobieramy jeden plik to dostajemy plik, jeśli więcej niż 1 to dostajemy ZIPa z plikami

I takie piszemy testy integracyjne, pokrywając te wspomniane sytuacje. Nasuwa się tutaj pytanie o klasy równoważności zestawów testowych. Np. należałoby zrobić test parametryzowany który sprawdza czy działa zarówno dla roli X jak i roli Y, nie wystarczy test tylko dla jednej z ról. Teraz pytanie czy rola ma jakieś powiązanie z tą obsługą 1 lub wielu plików? Nie ma, więc nie ma sensu sprawdzać każdej kombinacji. Dalej czy jest różnica między 2 a 200 plikami? Wygląda że nie, więc nie musimy sprawdzać czy system obsłuży 2,3,4,5,.... plików, wystarczy nam jakiś jeden reprezentatywny test dla sytuacji "wiele plików" (i jeden dla jednego pliku). Możemy ewentualnie sprawdzić jakieś skalowanie i zrobić sobie mały i duży request.

Tyle na poziomie API. Ale załóżmy teraz że na poziomie implementacji requesty o <10 plików i >10 plików są realizowane trochę inaczej, bo np. potrzebujemy zlokalizować pliki w naszym systemie i w tym celu używamy funkcja1 albo funkcja2 -> jedna działa szybciej dla małej liczby druga szybciej dla dużej. Widać ze na poziomie API nie bardzo mamy jak coś takiego sprawdzić, bo system zadziałałby nawet gdyby była tylko jedna z tych funkcji.
Tutaj powinniśmy zrobić test jednostkowy, bo chcemy sprawdzić czy kod robi to programista spodziewa się że robi. Więc możemy napisać sobie testy jednostkowe które upewniają się, ze faktycznie dla < N plików logika idzie jednym torem a dla >N drugim torem.

Podsumowując: moim zdaniem nie wystarczy żaden "happy path" - potrzeba pokryć wymagania testami.

0
hauleth napisał(a):

Jak na moje tak. Doskonałe przykłady tego są na różnych memach jak ten:

Testy jednostkowe przejdą, ale już w spięciu z innymi komponentami mogą doprowadzić do zmian, które spowodują błędne zachowanie aplikacji.

No nie do końca. W teście jednostkowym sprawdzisz czy m.in możesz przesunąć klamkę, jak pozycja klamki wpływa na możliwość otwarcia okna, jak szeroko możesz otworzyć pojedyncze okno itd.
Test integracyjny obejmujący dwie ściany i dwa różne okna to już zupełnie inne przypadki użycia takie jak na obrazku czyli zależność możliwości otwarcia okien względem ich umiejscowienia na ścianach oraz umiejscowieniu ścian względem siebie.

0

@Shalom: ciekawe rozróżnienie jeśli chodzi o ten przykład z implementacją zależną od wydajności.

Wiesz, mówiąc ogólnie, im dłużej czytam o testowaniu oprogramowania, tym bardziej mam wrażenie, że testuje się nie po to, by dany system był niezawodny, a po to, by usterek, na które można natrafić, było jak najmniej. Nie jest bezpośrednim celem wyeliminowanie wszystkich błędów.

W związku z takim podejściem nie widzę jakichś sztywnych "poziomów", które identyfikowałyby poszczególne typy testów. Nabieram przez to takiego wrażenia, o jakim pisze @jarekr000000 , czyli – testy to testy. Jedyne sztywne rozróżnienie między integracyjnymi a jednostkowymi widzę takie, że integracyjne testują integrację, z samej definicji – czyli zawsze więcej niż jedna jednostka musi brać udział. (Co do innych typów testów to nie wiem, nie zastanawiałem się nad nimi).

Trochę zresztą szkoda, jak dla mnie. Chciałoby się powiedzieć: poziom niezawodności systemu nie jest funkcją tego, w jaki sposób się z niego korzysta. A tu wychodzi, że raczej jest. Jak m.in. Ty piszesz – wymagania to podstawa testów integracyjnych. Bez świadomości przypadków użycia – zakładanych lub faktycznych – zdaje się, że nie można napisać dobrych testów. Zresztą już samo stwierdzenie "dobre testy" jest dla mnie dziwne. Ale chyba tu pasuje…

Jeszcze muszę się zastanowić.

10

Trochę offtop, ale IHMO ta cała piramida testów (i odwrócona piramida testów) to pic na wodę. Dlaczego o tym mówimy? Bo pokazywali nam to na konferencjach. A czemu nam to pokazywali? Bo piramida ładnie wygląda na slajdach.
Chyba widziałem wszystkie możliwe ułożenia testów (za wyjątkiem piramid):

  • Pracowałem w firmach gdzie istniały tylko testy manualne.
  • Widziałem firmę gdzie istniały tylko testy manualne i jednostkowe, bo testerzy nie mieli czasu pisać testów systemowo-akceptacyjnych.
  • Pracowałem w firmie gdzie nie dało się powiedzieć czy jest więcej jednostkowych czy systemowo-akceptacyjnych bo programiści pisali swoje testy, a testerzy swoje.
  • Pracowałem w firmie gdzie była niechęć do testów jednostkowych, a programiści i testerzy wspólnie pisali testy integracyjno-akceptacyjne

Co do samych definicji to nie widziałem żadnego porządnego papieru, który określałby co to jest jednostka. Metoda/funkcja? Klasa/moduł? Pakiet? Mikroserwis? Mikroserwis z własną bazą danych? Wszystkie te sprzeczne definicje można spotkać na konferencjach. Niektórzy wprost mówią że definicje się zmieniły odkąd mamy mikroserwisy. Jeśli ktoś ma uznany papier z porządną definicją z chęcią przeczytam.

Które testy osobiście uważam za najlepsze? Te które są szybkie, ale jednocześnie testują maksymalnie dużo kodu. Dla mnie takimi testami są testy na poziomie mikroserwisu z prawdziwą bazą danych postawioną w dockerze. Jeśli czegoś w prosty sposób nie da się postawić w dockerze to mockuję. Albo na poziomie http, albo dostarczam alternatywną implementację klienta.

3
KamilAdam napisał(a):

Chyba widziałem wszystkie możliwe ułożenia testów (za wyjątkiem piramid):

  • Pracowałem w firmach gdzie istniały tylko testy manualne.
  • Widziałem firmę gdzie istniały tylko testy manualne i jednostkowe, bo testerzy nie mieli czasu pisać testów systemowo-akceptacyjnych.
  • Pracowałem w firmie gdzie nie dało się powiedzieć czy jest więcej jednostkowych czy systemowo-akceptacyjnych bo programiści pisali swoje testy, a testerzy swoje.
  • Pracowałem w firmie gdzie była niechęć do testów jednostkowych, a programiści i testerzy wspólnie pisali testy integracyjno-akceptacyjne

Prawdziwy szczęściarz z Ciebie, że spotykasz testy jednostkowe w swoich pracach, ja natrafiam głównie na testy klas.

Podoba mi się podejście z trzeciego punktu. Pracowałem kiedyś w taki sposób, i bardzo dużo rzeczy udawało się wyłapywać dzięki temu, że programiści robili swoje testy, a testerzy swoje. Różne punkty spojrzenia na tę samą kwestię dają więcej efektów nawet niż pomysły latający po konferencjach architektów.

Co do samych definicji to nie widziałem żadnego porządnego papieru, który określałby co to jest jednostka. Metoda/funkcja? Klasa/moduł? Pakiet? Mikroserwis? Mikroserwis z własną bazą danych?

Jak rozumiem, to chciałbyś dostać precyzyjną definicję i wzór działania, żeby się tego trzymać i móc pracować w oparciu o teorię. To by było bardzo wygodne, bo wówczas nie trzeba byłoby w ogóle analizować ani się nad niczym zastanawiać. Niestety, to tak nie działa, bo programowanie to nie jest matematyka, tu trzeba działać bazując na intuicji i doświadczeniu, a nie wzorach i definicjach.

Czasami jednostką jest metoda, czasami klasa, czasami grupa klas (być może nazywasz to pakietem). W przypadku języków obiektowych interfejsem jednostki będzie publiczna metoda. Jednostka to jest coś, co jest w stanie samodzielnie spełnić jakąś funkcję, np. zmienić litery w stringu na duże, policzyć całkę albo przetworzyć koszyk sklepowy na fakturę.
W testowaniu jednostkowym chodzi o to, żeby dało się testować daną jednostkę w izolacji od reszty elementów danego systemu, czyli nie tylko innych jednostek kodu, ale też baz danych, plików, protokołów sieciowych, webserverów, czasu, itd. Celem jest zweryfikowanie, czy Twój kod (algorytm) działa, nie czy poprawnie działa komunikacja z jakimś API, czy masz dostęp do katalogu, czy baza danych istnieje, itd. Z tego też powodu miejscem przechowywania danych w trakcie przeprowadzania testu jednostkowego jest pamięć, nie trwałe nośniki, bo one są elementami zewnętrznymi, więc nie są częścią jednostki.
Obrazowo mówiąc - jeśli zapisujesz fakturę do bazy danych, aby obliczyć sumę podatku VAT jej pozycji, to to nie jest test jednostkowy. Jeśli uruchamiasz webserwer i bazę danych, to to nie jest test jednostkowy. Dlatego też mikroserwis z bazą danych to nie jest jednostka w sensie testów jednostkowych.

Które testy osobiście uważam za najlepsze? Te które są szybkie, ale jednocześnie testują maksymalnie dużo kodu. Dla mnie takimi testami są testy na poziomie mikroserwisu z prawdziwą bazą danych postawioną w dockerze.

Owszem, takie (ja je nazywam prawdopodobnie niepoprawnie integracyjnymi) są najlepsze, czasami jednak niewystarczające. Jest sporo sytuacji, w których ma sens pisanie testów jednostkowych. Oczywiście raczej przed właściwą implementacją niż po niej, bo dopisywanie testów później to często strata czasu.

0
iksde napisał(a):

Wg mnie testy integracyjne muszą być, a jednostkowe mogą/powinny być.

Ale czy nie jest tak, że jak olewasz testy jednostkowe, to masz duuużo więcej rzeczy do przetestowania w integracyjnych???

Weźmy 3 unity po 2 ścieżki/przypadki do przetestowania, a po zintegrowaniu masz już 8 ścieżek/przypadków (zakładamy niezależność, co oczywiście jest uproszczeneim). Widać przecież, że to rośnie wykładniczo...

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