Wątek przeniesiony 2020-03-08 22:38 z Java przez Shalom.

Granice testów jednostkowych

0

Czy pisząc test dla danej metody z jakiegoś serwisu uderzając do testowej bazy danych to nadal test jednostkowy? Czy już integracyjny? Gdzie zaczynają są granice testów jednostkowych? Czy pisząc taki test powinno się mockować odpowiedź czy używać bazy danych?

4

Test jednostkowy odnosząc się do zależności kodu produkcyjnego używa mock-ów.
Jeśli test sięga po faktyczną bazę danych to albo jest to test integracyjny, albo ułomny test jednostkowy.

Problem leży tutaj:

Czy pisząc test dla danej metody z jakiegoś serwisu

Bo powinno być na odwrót, piszesz kod do testu.

Bez mock-ów nie da się napisać testu sprawdzającego obsługę błędów, bo sensowne doprowadzenie np bazy danych do raportowania błędu jest bardzo trudne.
Bez mock-ów testy są boleśnie powolne.

1

Wydaje mi się że o teście integracyjnym zaczynamy mówić gdy do odpalenia testu potrzebna jest jakaś baza w pamięci, jakiś mock np. do zewnętrznego serwisu, ewentualnie plik z danymi wyjściowymi. Pytanie czy jak zamiast H2 użyje implementacji repo w hashmapie to czy to integracja czy jednostka? Wydaje mi się że wyznaczanie takiej granicy jest zbędne w życiu ale jakbym miał przyjmować kryterium to zapewne byłby to czas uruchamiania testów.

Tak czy owak: olej ten podział bo to "zależy".

10

Nie wiem jak teraz jest w Google, ale w książce "Testuj oprogramowanie jak Google. Metody automatyzacji" jest napisane że w Google w ogóle odeszli od terminów "testy jednostkowe, integracyjne i systemowe". Zamiast tego używają terminów "testy małe (szybkie), średnie i duże (wolne)" I to jest ihmo bardzo dobra definicja.
Z innych opinii słyszałem że jeśli sam zarządzasz bazą to to są testy jednostkowe, a jak baza jest systemem zewnętrznym to to są testy integracyjne.

Przy okazji spostrzeżenie, H2 in memory jest zwykle używana jako mock (stub?) prawdziwej bazy (np PostgreSQLa). Dlaczego nie uznawać tego za test jednostkowy?

1
MarekR22 napisał(a):

Bez mock-ów nie da się napisać testu sprawdzającego obsługę błędów, bo sensowne doprowadzenie np bazy danych do raportowania błędu jest bardzo trudne.

Nie do końca rozumiem ten punkt. Możesz rozwinąć?

Bez mock-ów testy są boleśnie powolne.

Większość moich testów, które korzystają z bazy danych albo serwisów webowych działają po HTTP z nagranymi odpowiedziami i z prawdziwą bazą danych w pamięci. Większość testów nie trwa więcej niż 10 milisekund. Jak dochodzi trochę HTTP to faktycznie może się wydłużyć do 100 ms, bo startuje serwer itp.

2

Pytania o nazewnictwo w programowaniu są z reguły bezcelowe. Jeśli pytasz o samą nazwę - znajdą się tacy którzy powiedzą że to unit, są tacy którzy powiedzą że nie.

Jeśli masz jakieś dalsze pytania, takie które nie zależą od nazewnictwa ale innych rzecz to rozwiń je.

8

test dla danej metody z jakiegoś serwisu

To jest błąd a nie test ;) Testuje się funkcje systemu a nie metody czy klasy. Testy jednostkowe na takim poziomie w 95% nie są w ogóle ani potrzebne ani sensowne. Pisze się je najwyżej jak masz jakiś skomplikowany kawałek (nie wiem, jakieś parsowanie na przykład) i chcesz je przemaglować we wszystkie strony. W pozostałych przypadkach dużo więcej sensu ma test integracyjny który idzie od entry pointu aplikacji i robi test całej funkcji systemu.

0

Pytanie poboczne:
Jeżeli podnoszę cały kontekst springa, z bazą dockerową podnoszoną na czas testu, i uderzam pod konkretny endpoint wykonując przy tym jakieś asercje - można nazywać to testem integracyjnym? ;)

0
Shalom napisał(a):

test dla danej metody z jakiegoś serwisu

To jest błąd a nie test ;) Testuje się funkcje systemu a nie metody czy klasy. Testy jednostkowe na takim poziomie w 95% nie są w ogóle ani potrzebne ani sensowne. Pisze się je najwyżej jak masz jakiś skomplikowany kawałek (nie wiem, jakieś parsowanie na przykład) i chcesz je przemaglować we wszystkie strony. W pozostałych przypadkach dużo więcej sensu ma test integracyjny który idzie od entry pointu aplikacji i robi test całej funkcji systemu.

Tylko, że jak masz metodę, z kilkoma argumentami to żeby przetestować ją np. przy okazji testowania widoków to musiałbyś pisać X więcej testów dla tego widoku. Natomiast jak przetestujesz dodatkowo osobno to możesz skupić się na testowaniu tej metody porządniej i nie umkną Ci np. wartości skrajne itd.

0

Moim zdaniem, jeżeli test łączy jakąs funkcjonalność (zalozmy backendową) z frontendem, jakimis reponsami JSONowymi, albo bazą danych, która nie jest zamockowana (typu jakieś prepared h2) to jest to test integracyjny.

Oczywiście przy zalozeniu ze dzielimy testy na jednostkowe, integracyjne etc.

1

@anonimowy

masz metodę

Nie masz bo testujesz funkcje systemu a nie metody albo ich argumenty. Poza tym twój argument jest inwalidą bo takie testy I TAK musisz mieć, bo inaczej nie masz żadnej pewności że coś faktycznie działa. To sie nazywa testy akceptacyjne.
Tak jak pisałem czasami są bardziej złożone kawałki kodu które trzeba przetestować jednostkowo, ale to jest kilka %

11

Jak już @Kamil Żabiński napisał:

Testy

Testy - jak są względnie szybkie i testują funkcjonalność to sa dobre - nawet jak podnoszą bazę danych, kontekts springowy, albo tylko ciśnienie.

Zawsze mnie dziwiło jak ktoś upiera, się że test z bazą w pamięci to integracyjny, a jak jest ta baza jest zamokowana hashmapą to już nie (mimo, że ta baza w pamięci (np. Key-value) mogła by być hashmapą...).

Jeżeli jakiś kawałek kodu robi wszystko grzebiąc po bazie danych to jej mokowanie jest stratą czasu.
Uważam pojęcie testów jednostkowych (i integracyjnych) za problematyczne, zwodnicze - niepotrzebnie zamieszanie.

Testy to testy.

1
Shalom napisał(a):

@anonimowy

masz metodę

Nie masz bo testujesz funkcje systemu a nie metody albo ich argumenty. Poza tym twój argument jest inwalidą bo takie testy I TAK musisz mieć, bo inaczej nie masz żadnej pewności że coś faktycznie działa. To sie nazywa testy akceptacyjne.
Tak jak pisałem czasami są bardziej złożone kawałki kodu które trzeba przetestować jednostkowo, ale to jest kilka %

To nie rozumiem. Nie masz metod, które mają jakieś zasady biznesowe w sobie? Np

def get_member_time(user):
    if user.has_expired_time() or user.exceed_seeds():
        return 6
    if user.type not in ['asd', 'qwe']:
        return 8
    return 11


def get_parent_time(user):
    if (user.has_expired_time() and user.exceed_seeds()):
        return 6
    if user.type in ['asd', 'qwe']:
        return 5
    return 13


class SampleView(View):
    def get(self, request, *args, **kwargs):
        self.object = self.get_object()

        if get_member_time(self.object) > 10 or get_parent_time(self.object) > 10:
            # do someting else
        return super().get(request, *args, **kwargs)

W tym kodzie testowałbyś tylko SampleView? To albo musiałbyś testować if get_member_time(self.object) > 10 or get_parent_time(self.object) > 10: też linię na X sposobów albo mniej i po prostu przetestować metody get_time osobno

6

@anonimowy: tu choci o podejście i o cel.

Nie jest celem testowanie funkcji, metod czy klas. Tylko, o sprawdzenie czy dany komponent spełnia wymogi, zachowuje się poprawnie.
Zwykle to oznacza, że masz w komponencie jakieś metody (api), kóre wywołujesz i sprawdzasz czy wyniki się zgadzają.
Ale jeśli wyniki się zgadzają nawet bez istnienia metod to po prostu win-win. Kodu mniej, utrzymywać lżej.

W podanym przykładzie get_parent_time mogłoby być np, prywatne i wystarczyłby test na poziomie metody wyżej.

1

Tylko jeśli chcesz mieć przetestowane wszystkie możliwości to musiałbyś mieć tych testów od groma

6

@anonimowy: dokładnie tyle samo. Jeśli jako możliwości masz na myśli ścieżki. Jeśli masz na myśli wszystkie inputy (np. dla Int) ... to żadne testowanie Ci tego łatwo nie zapewni.

Jeśli testujesz tylko "prywatne" elementy to masz masz często problem przy refaktoringu.
Po przeoraniu kodu, podzieleniu metod, przeniesieniu ich do innych klas trzeba całkiem przeorać testy... a to jakby niweczy samą ideę testów.

0

W tym przypadku tyle samo ale jeśli np. w widoku doszedłby jeszcze jeden warunek to rośnie podwójnie

3

To co piszesz ma sens, ale ja to raczej widzę w drugą stronę - tak tworzę system, aby jego kawałki (metody, klasy) były sensownie testowalne. Nawet jeśli przez to czasem muszę zrobić coś pakietowo, modułowo dostępne (zamiast prywatnie). Znowu - to tylko kwestia intencji. Na koniec możemy mieć całkiem podobny kod.

3

@anonimowy spoko, to pisz sobie testy na poziomie takich mikro metod i potem przy każdej zmianie będziesz poświecać 2x więcej czasu na ich poprawnienie, mimo ze SYSTEM nadal działa i funkcje nadal działają. Tylko że to taki przykład złego testu -> jeśli system nadal działa a testy failują, to testy są zrypane (analogicznie oczywiście jeśli testy zielone a system nie działa to też można te testy zaorać).
Jasne że zmiany w kodzie pociagają zmiany w testach, ale jeśli musisz przepisać testy za każdym razem jak coś się zmienia, to jaka jest ich wartość? Co one ci tak realnie dają? Bo jak dla mnie takie testy sprawdzają tylko czy kod wygląda tak samo jak kiedy pisałem testy.

Jeszcze raz: ważne są funkcje systemu a nie jego wewnętrzne detale implementacyjne. To czy mój system ma serwis X albo metodę Y nie ma żadnego znaczenia. Ważne czy realizuje potrzebe biznesową.

edit:

W tym kodzie testowałbyś tylko SampleView? To albo musiałbyś testować if get_member_time(self.object) > 10 or get_parent_time(self.object) > 10: też linię na X sposobów albo mniej i po prostu przetestować metody get_time osobno

A jak inaczej? Przecież bez tego wcale nie wiesz czy to w ogóle działa :D to get_time może działać ok, a mimo wszystko w kupie to będzie działać źle. Klasyczne: https://ilkinulas.github.io/assets/integration_tests/no_integration_test.gif

0

Ale się temat rozkręcił! A prawda jest taka że to są wszystko miękkie granice. Czasem kilka testów trzeba napisać ale nie popadałbym w paranoję "100% coverage". Taką paranoję mają zwłaszcza ludzie którzy właśnie się dowiedzieli o testach jednostkowych ale nigdy ich na oczy w prawdziwej firmie nie widzieli.

0
Shalom napisał(a):

A jak inaczej? Przecież bez tego wcale nie wiesz czy to w ogóle działa :D

No to wtedy takie testy będą wiecznie trwać

4

Jednostką jest system :]

Testuejsz jednostkowo -> testujesz system

0

Na to pytanie nie ma jednej odpowiedzi, wiele zalezy od kontekstu.

Dla przykładu bezpośrednie testy wewnętrznych rzeczy to wrażliwa sprawa i lepiej dla systemu gdyby zostały przetestowane nie wprost przez publiczne API, prawda?

Problemem dużych projektów jest fakt, że są zbyt duże. Jeśli wewnętrznie czuje się potrzebę przetestowania jakiejś wewnętrznej implementacji (bo łatwiej pisze się szczegółowe przypadki) to zastanów się czy nie lepiej byłoby wybranego kawała kodu zmienić w osobną bibliotekę.

I wtedy warto zadać sobie pytanie ile mamy tych API?

Jeśli masz osobną bibliotekę i napiszesz dla niej testy to mamy zbrodnię czy już jej nie mamy? A przecież przed chwilą kod tej biblioteki stanowił jakiś wewnętrzną część systemu :-)

Wydzielony kod nawet gdyby był ogólnego przeznaczenia (w sumie tym lepiej!) to nie jest koszt na zaś. Jeśli ktoś wydzieli z kodu implementacje listy, jakiejś matematycznej funkcji to pod wpływem zmiany wymagań wybrany fragment kodu nie straci swojego pierwotnego znaczenia. Tak samo jak liczenie sumy to nadal suma.

0

W sumie mam jeszcze jedną myśl wartą odnotowania à propos granic testów.

Granice testów (jakichkolwiek!) nie powinien wyznaczać ich rodzaj pokroju integracyjne/nieintegracyjne, szybkie/wolne, publiczne/prywatne, a fakt z jakim wiążą się ryzykiem.

Myślę, że ryzyko z projektem ciężej jest pojąć osobie, która co miesiąc robi to samo za to samo. Zupełnie inaczej podejście wygląda, gdy masz ograniczony budżet.

To nie jest tak, że wraz z kolejnym testem rośnie wartość projektu. W praktyce test to nic innego jak inwestycja, która oznacza pewien koszt i też jak inwestycja może w ogóle się nie zwrócić.

Programista powinien potrafić ocenić skalę ryzyka. Powinien wiedzieć co warto, a co nie warto testować. Powinien umieć to argumentować albo chociaż odwlekać w czasie aż lepiej zrozumie sytuację.

Programista, który tego nie czuje, i pisze testy jak z automatu bo task trzeba spiąć (lub w ogóle ich nie pisze) to strata. Bezmyślne testy nikomu nie służą.

Już lepiej ten czas na testy przeznaczyć na przemyślenie rozwiązania. Może jesteś w stanie coś uprościć, wpłynąć na wymaganie albo chociaż lepiej poznać potrzeby biznesowe jakie za nimi stoją :-) Taka zmiana jest 1000 razy więcej warta niż kolejny bezmyślny test.

3

Ja to tylko zostawie

3

Nie dziwi mnie, że temat jest tak rozgrzany. Na moje oko bezmyślne testowanie z moksturbacją to jeden z większych raków dzisiaj.

Zasady są proste:

  1. Dzielić system per usecase / agregat / funkcjonalność / komponent co kto ma u siebie.
  2. Ukrywać bebechy i eksponować wyłącznie publiczny interfejs do naszego usecase / agregatu / funkcjonalności / komponentu co kto ma u siebie.
  3. Testy pisać dla tego publicznego interfejsu, bo zwykle się rzadko zmienia. Bebechy często.
  4. Bazy danych, pliki, Resty stawiać w kodzie. Większość tego dzisiaj można postawić w pół sekundy i pięcioma linijkami w Javie. Pomaga tu np. localstack, Test Containers etc.
  5. Nic tylko refaktorować bebechy później, podbijać wersję Javy, przechodzić na GralVM i z Oracle na Postgresa :) Sama przyjemność.
  6. Staramy się testować wszystkie wartościowe dane wejściowe i stany, ale bez przesady. Aby nie duplikować kodu sprawdzają się testy parametryczne.

Ja jeszcze eksperymentuję z takim podejściem modułowym:
mózg-domain
mózg-application
mózg-tests

Czyli testy mam do domeny napisane w osobny module i importuję je w <scope>test</scope> zarówno w module domain i application. Z tym, że w domain podstawiam do nich mocki, a w application jakieś in-memory + docker cuda. Dzięki czemu lokalnie robię sobie szybkie TDD na mockach, a przed commitem o kilkanaście sekund wolniejsze mvn test. Z tym, że nie testowałem tego na PROD nigdzie. :) Unikam też w module domain zależności do połowy Maven Central w testach.

Co o tym sądzicie?

0

@Shalom: @jarekr000000: @semicolon
Czyli waszym zdaniem trzeba testować tak (każdą ścieżkę):
https://4programmers.net/Pastebin/15262

A testowanie w ten sposób:
https://4programmers.net/Pastebin/15261

To zbrodnia? A co jak by doszły jeszcze dwa warunek do metody get_count_pokemons? Wtedy trzeba by dodać 14 kolejnych testów. A jeszcze jakby jakiś nowy warunek na status doszedł to znowu mnożymy liczbę testów a to przecież raptem kilka warunków

1

@anonimowy: Jak zrobisz ten test parametryczny to będzie tylko jedna metoda i zestaw inputów/outputów, w praktyce jeszcze mniej kodu niż te twoje unit testy. A te unity w ogóle są słabe, bo nie wiadomo co testują. Nazwa testu powinna jasno określać co chcesz sprawdzić tak żeby wywalenie sie jasno komunikowało co nie działa. Jak masz 1000 asercji i realnie testujesz zupełnie różne rzeczy, to jest słabe.
Teraz wyobraź sobie że leci refaktor tego kodu i np. metoda get_is_seedable znika i zastępujemy to wszystko jakąś inną logiką, tak że wszystko nadal działa. Co się teraz stało?
W jednym przypadku masz cały czas testy chroniące przed regresją, bo sprawdzają czy funkcja systemu działa. W drugim masz praktycznie tylko testy do zaorania bo nawet sie nie skompilują. Spoko, możesz napisać nowe, kto bogatemu zabroni, tylko nie wiesz czy masz regresje czy nie...

0

@Shalom No nie jestem w stanie zrozumieć twierdzenia, że nie ma tu regresji. Przecież mam test, który sprawdza widok w kilku przypadkach a jak get_is_seedable znika to znika również test do niego (co jest jak najbardziej ok i normalne, bo już się nie przyda po prostu. Tak samo jak byś wywalił logikę biznesowa to zmieniasz testy w Twoim przypadku).

Z tym, że jest dużo asercji to się zgodzę ale pisałem to na szybko i byle jak, chodziło mi o pokazanie koncepcji. Ale to jakby osobna kwestia.

W Twoim przypadku jeśli doszło by do jeszcze kilku innych warunków to przecież żeby każdą ścieżkę przerobić byłoby potrzebne tysiące testów a w niektórych przypadkach setki tysięcy czy miliony, to nie miałoby możliwości szybko działać.

Mógłbyś pokazać przykład takiego sparametryzowanego testu na moim przykładzie jak to powinno wyglądać według Ciebie?

Ja oczywiście nie neguję waszego podejścia do testowania ale uważam, że testowanie metod też jest jak najbardziej przydatne i pozwala szybciej wprowadzać zmiany

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