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:
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.
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.
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).
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.
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.
@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
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?
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