Nie rozumiem sensu unit testów?

0

Typowe wymogi pisania unit testów:

  • Mają wykonywać się szybko
    • Metodologia pisania oprogramowania: * Pisz unit test * uruchom testy * pisz małą zmianę * uruchom testy * refaktoryzuj * uruchom testy. Jeśli testy będą trwały więcej, jak kilka sekund max, to taka metodologia nie ma sensu
  • Dlatego:
    • Mają testować tylko pojedynczą metodę, a już na pewno nic więcej, jak klasę
    • Wszystko inne ma być zamockowane

Piramida testów:

  • Najwięcej unit testów, w drugiej kolejności testów integracyjnych, natomiast klikania ręcznie najmniej, jak to możliwe
  • Trzymając się powyższej metodologii unit testów będzie cała hałda, i dobrze

Zadania testów:

  • Walka z regresjami przede wszystkim
    • Zmiana danej metody / klasy ma nie psuć jej specyfikacji w sposób niezamierzony
    • Zmiana danej metody / klasy ma nie psuć innych, pozornie niezwiązanych klas w sposób niezamierzony

Unit testy wydają mi się przeczyć obu tym zadaniom?

  • Unit testy nie chronią przed niezamierzonymi regresjami w obrębie metody / klasy, którą testują
    • Jeśli kod danej metody / klasy ulega zmianie, to zmianie musi często ulec także jej unit test. Zatem stary unit test jest wyrzucony i nic nie testuje
  • Unit testy nie chronią przed niezamierzonymi regresjami w innych miejscach kodu
    • Jak mają chronić, skoro wszystkie inne klasy są zamockowane?
  • Refaktoryzacja unieważnia unit testy
    • Przeorganizowuję metody w klasie - muszę przepisać wszystkie / większość unit testów tych metod. Zmieniam publiczny interfejs klasy - muszę przepisać unit testy tego interfejsu. Stare testy są wyrzucone, więc nic nie testują i nie chronią przed regresjami w kodzie, który właśnie wprowadzam

Remedium: Tylko testy integracyjne / end to end?

  • Tzw. „mądrzy ludzie” napisali piramidę testów, według której testów integracyjnych / e2e ma być wyraźnie mniej, niż unit testów - może mieli mądre ku temu powody?
  • Testy integracyjne potrafią długo trwać - dokładne testowanie każdego elementu specyfikacji programu za ich pomocą wykluczy częste uruchamianie testów
  • Testowanie działania głęboko zagnieżdżonych metodek za pomocą testów e2e może wymagać pisania skomplikowanych testów
    • Już dokładne pisanie unit testów zajmuje b. dużo czasu, pisanie testów j.w. będzie zajmowało chyba nieakceptowalne ilości czasu

Naprawdę nie rozumiem?

12
YetAnohterone napisał(a):
  • Dlatego:
    • Mają testować tylko pojedynczą metodę, a już na pewno nic więcej, jak klasę
    • Wszystko inne ma być zamockowane

To błędne myślenie. Testy jednostkowe mają testować jednostkę, nie klasę/metodę.

  • Najwięcej unit testów, w drugiej kolejności testów integracyjnych, natomiast klikania ręcznie najmniej, jak to możliwe

To zależy od rodzaju softu, który tworzymy.

  • Unit testy nie chronią przed niezamierzonymi regresjami w obrębie metody / klasy, którą testują
    • Jeśli kod danej metody / klasy ulega zmianie, to zmianie musi często ulec także jej unit test. Zatem stary unit test jest wyrzucony i nic nie testuje
  • Unit testy nie chronią przed niezamierzonymi regresjami w innych miejscach kodu
    • Jak mają chronić, skoro wszystkie inne klasy są zamockowane?
  • Refaktoryzacja unieważnia unit testy
    • Przeorganizowuję metody w klasie - muszę przepisać wszystkie / większość unit testów tych metod. Zmieniam publiczny interfejs klasy - muszę przepisać unit testy tego interfejsu. Stare testy są wyrzucone, więc nic nie testują i nie chronią przed regresjami w kodzie, który właśnie wprowadzam

To co opisujesz, to jest właśnie błędne zastosowanie unit testów, sprowadzające się do pisania testów oddzielnych klas, a nie testów jednostek.

Jednostka to jest coś, co dostarcza samodzielnie jakąś funkcjonalność. To może być metoda z jedną linijką a może to być zestaw kilkunastu kooperujących ze sobą klas. Jeśli masz poprawnie zdefiniowane API swojej jednostki i napisane testy do tego API, to możesz ją dowolnie ciąć na klasy i metody, bez żadnych problemów.

3

Piramida testów to bullshit, generalnie w IT mam taką zasadę, że jak ktoś wymyśla jakiś obrazek do swojej teorii to chce ci coś sprzedać, bo nie chodzi o konkrety tylko o marketing.
Sam podział na unity/integrację jest bez sensu, bo czym tak naprawdę jest unit. Testy można dzielić na cechy takie jak np. czy uderzają do zewnętrznych zasobów albo, czy są szybkie. Termin Unit test to próba sklastrowania testów o wielu cechach, które w skrócie można nazwać szybkie testy testujące samodzielny kawałek kodu. Tylko to zgrupowanie jest słabe, bo do praktycznie każdej cechy unit testów można znaleźć wyjątek. To tak jakbyś spróbował posegregować ludzi rasowo: wiadomo, że biały człowiek to biały człowiek a murzyn to murzyn, ale jak zaczniesz grzebać głębiej to będziesz miał więcej pytań niż odpowiedzi

Według mnie najważniejsze testy to te, które testują jak twoja aplikacja działa naprawdę. Czyli dla aplikacji CLI będzie to uruchamianie aplikacji wraz z linią komend/stdin/potrzebnymi plikami. Dla serwera to będzie uderzanie do endpointów. Tylko takie testy gwarantują, że jakakolwiek zmiana (np. refactor, zmiana frameworku, zmiana bazy) nie psuje wcześniejszych założeń.

Co do trwania testów to niestety jest to patologia, bo mało ludzi z tym walczy a da się naprawdę dużo zrobić. Pamiętaj, że to czy będziemy pisać integracyjnie zależy głównie od tego ile te testy trwają. I tak:

  • testy trwają długo, bo jest ich dużo a nie wiem które trzeba puścić. Niestety jest to problem i w zasadzie rzadko spotykam dobre rozwiązania. Fajnie robi to np. system do budowania Bazel. Dzięki wiedzy na temat zależności między modułami da się bardzo szybko wykryć która zmiana wpływa na które testy. Albo to, że jak usunę białą linię to testy się nie uruchomią, bo wynikowy artefakt będzie miał ten sam hash co poprzedni.
  • czas wstawania aplikacji: tutaj wychodzą patologie niektórych technologii np. Springa. Samo postawienie serwera HTTP w normalnych okolicznościach to powinny być milisekundy, no ale Spring to Spring i tester musi brać pod uwagę ten czynnik
  • zewnętrzne komponenty wstają długo, tutaj niestety mało da się zrobić poza zmianą komponentów, albo sztuczkami typu długożyjące kontenery. Przykładowo taki postgres postawiony przez testcontainers wstaje 2 sekundy; długo, ale akceptowalnie. Z drugiej strony taki RabbitMQ wstaje 10s albo i więcej, co jest dla mnie tragedią.
  • często zdarza się tak, że testy integracyjne nie są dostosowane do uruchamiania równoległego. Mam taką obserwację, że w czasie pisania testów bardzo łatwo o to zadbać. Z drugiej strony migracja istniejących "złych" testów jest często niemożliwa
5

Właśnie odkryłeś czemu większość unit-testów pisanych w projektach jest bezużyteczna :)

Tzw. „mądrzy ludzie” napisali piramidę testów, według której testów integracyjnych / e2e ma być wyraźnie mniej, niż unit testów - może mieli mądre ku temu powody?

Ja znam mądrych ludzi (jestem jednym z nich :P) którzy twierdzą ze taka piramida nie ma zadnego sensu. Chyba ze masz bardzo dużo jakiejś logiki obliczeniowej/algorytmicznej w projekcie i potrzebujesz weryfikować implementacje tych algorytmów.

Testy integracyjne potrafią długo trwać - dokładne testowanie każdego elementu specyfikacji programu za ich pomocą wykluczy częste uruchamianie testów

Mitologia. Mam serwisy gdzie takich testów jest powiedzmy ~100 i wykonują się kilka sekund.

Testowanie działania głęboko zagnieżdżonych metodek za pomocą testów e2e może wymagać pisania skomplikowanych testów

Nie testuj metodek tylko funkcje systemu. Jeśli chcesz koniecznie przetestować metodę albo kawałek kodu, to napisz unit test.

Już dokładne pisanie unit testów zajmuje b. dużo czasu, pisanie testów j.w. będzie zajmowało chyba nieakceptowalne ilości czasu

Mitologia. Jak napiszesz sobie jakiś sensowny DSL do konfigurowania stanu aplikacji to będzie się je pisało szybciej niż unit testy.

Zapraszam: https://github.com/pharisaeus/almost-s3

0

Ajj, za późno się zorientowałem, że powinienem był raczej pisać post, niż "strumień świadomości" w komentarzach, przepraszam za bałagan. Usuwam komentarze, przepisuję je na post. O ile jeszcze zdążę.

Załóżmy, że piszę program pobierający dane z jakiegoś 3rd party API. Czy uruchamianie testów powinno uderzać w to 3rd party API, czy też w stub tego 3rd party API, który dla celów testowania napiszę?

Jeśli 3rd party API, to testy nie działają, bo 3rd party API poszło w dół. Źle?

Jeśli mój stub - tracę dużo czasu, bo piszę tego mocka, a poza tym testy nie testują, czy mój program działa, bo ja mogę wcale nie rozumieć dziwactw tego 3rd party API. Więc chyba jednak powinny rzeczywiście uderzać w 3rd party API. Jakkolwiek jest to sprzeczne z tym, co "w internetach" czytałem nt. testów.

EDIT

Characteristics of a good unit test

  • Fast. It is not uncommon for mature projects to have thousands of unit tests. Unit tests should take very little time to run. Milliseconds.
  • Isolated. Unit tests are standalone, can be run in isolation, and have no dependencies on any outside factors such as a file system or database.
  • Repeatable. Running a unit test should be consistent with its results, that is, it always returns the same result if you do not change anything in between runs.

https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices

EDIT 2: Czemu teraz przyszło mi to na myśl: Zdarzyło się raz, że właśnie miałem taką sytuację i zastubowałem 3rd party API, ale szef stwierdził, że to źle, mam usunąć stuba i w ramach testów uderzać w to 3rd party API.

Co oczywiście spowolniło test suite.

0

Załóżmy, że piszę program pobierający dane z jakiegoś 3rd party API. Czy uruchamianie testów powinno uderzać w to 3rd party API, czy też w stub tego 3rd party API, który dla celów testowania napiszę?

@YetAnohterone: zależy co chcesz osiągnąć. Byłem w projekcie, gdzie 3rd party API było tak bardzo gówniane, że nie dało się go używać do testowaniu funkcji systemu. Oczywiście były testy, które go używały (bo chcesz przetestować, czy twój produkt działa a bez 3rd party API się nie da), ale były też testy, które testowały stuby (żeby wiedzieć, że to my zepsuliśmy a nie 3rd party API)

W przypadku testów 3rd party API stubowanie ma sens bo:

  • nie masz kontroli, ktoś z zewnętrznej firmy zmienił logikę a ty o tym nie wiesz
  • system może leżeć przez cały tydzień, co wtedy z developmentem?
  • słaby performance zewnętrznego serwisu, dobre odpowiedzi zwracają się w 1% zapytań (oparte na faktach)
  • piszesz logikę do systemu, który jeszcze nie istnieje. Słabo, ale zdarza się
  • musisz przetestować np. sytuację jak serwis zwróci HTTP 500. W tej chwili nie potrafimy znaleźć takiego przypadku, ale kto wie co będzie w przyszlości
0
slsy napisał(a):
  • nie masz kontroli, ktoś z zewnętrznej firmy zmienił logikę a ty o tym nie wiesz

Ale to był właśnie argument szefa, dlaczego należy uderzać w to API, a nie w mojego stuba.

Jeśli ktoś z zewnętrznej firmy zmienił logikę, a ja o tym nie wiem, to testy mają się nie udać, żebym się dowiedział i jak najszybciej dostosował mój program do nowej logiki tego 3rd party API.

0

Jeśli ktoś z zewnętrznej firmy zmienił logikę, a ja o tym nie wiem, to testy mają się nie udać, żebym się dowiedział i jak najszybciej dostosował mój program do nowej logiki tego 3rd party API.

@YetAnohterone: generalnie się zgadzam. Z drugiej strony taka sytuacja: gówniany kod, właśnie wszedł duży refaktor i testy przestały przechodzić. Normalnie spodziewałbyś się, że to refactor zepsuł a okazało się, że z kodem jest wszystko OK, ale zewnętrzny serwis coś popsuł. Znalezienie takiego błędu może być bardzo ciężkie

2

Właśnie niedawno skończyłem czytać "Working Effectively with Legacy Code By Michael Feathers" i tam jest ciekawa definicja:

unit test - A test that runs in less than 1/10th of a second and is small enough to help you localize problems when it fails.

Do definicji testów jednostkowych można różnie podejść, ale powyższe jest całkiem ok, pod warunkiem, że dorzucimy trzecią część - test jednostkowy nie zależy od jakiejkolwiek infrastruktury poza maszyną, na której jest odpalany.

Zdarzyło się raz, że właśnie miałem taką sytuację i zastubowałem 3rd party API, ale szef stwierdził, że to źle, mam usunąć stuba i w ramach testów uderzać w to 3rd party API.

Co oczywiście spowolniło test suite.

U nas kolega podzielił buildy, tzn. podzielił testy na te odpalane podczas głównego builda (jeśli padną to blokują publikowanie artefaktów) oraz dodatkowe odpalane w buildzie z dopiskiem IT w nazwie (od integration tests). Całkiem dobrze się to sprawuje. Robimy to z użyciem https://www.scala-sbt.org/ bo piszemy w Scali, ale w takim Mavenie da się pewnie zrobić coś podobnego. W sbt stosujemy https://www.scala-sbt.org/1.x/docs/Testing.html#Integration+Tests i obok src/test/scala, src/test/resources mamy src/it/scala, src/it/resources (w sumie jest to zgodne z https://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html ). Test z src/test/scala są odpalane podczas głównego builda, a testy z src/it/scala są odpalane podczas dodatkowego builda IT.

0

Ja uważam, że testy jednostkowe jak najbardziej są potrzebne.
Każda klasa ma swoją specyfikę i różne przypadki brzegowe,
które trzeba osobno przetestować. Nie wyobrażam sobie
testowania tego integracyjnie tj. wszystkich kombinacji
jakie mogą wystąpić pomiędzy tymi klasami.
Poza tym w dużym projekcie jak pracuje nad kodem wiele
osób to jak przetestuje klase jednostkowo to jestem bardziej
pewny, że nikt mi tam nic nie popsuje. Integracyjne też piszę
żeby wiedzieć, że system działa zgodnie z założeniami i realizuje
wymagane funkcje.

4
YetAnohterone napisał(a):

Typowe wymogi pisania unit testów:

  • Mają wykonywać się szybko
    • Metodologia pisania oprogramowania: * Pisz unit test * uruchom testy * pisz małą zmianę * uruchom testy * refaktoryzuj * uruchom testy. Jeśli testy będą trwały więcej, jak kilka sekund max, to taka metodologia nie ma sensu
  • Dlatego:
    • Mają testować tylko pojedynczą metodę, a już na pewno nic więcej, jak klasę
    • Wszystko inne ma być zamockowane

To się nazywa "premature optimization" jeśli testujesz " nic więcej, jak klasę", bo się boisz, że testy będą się dużo uruchamiać.
Możesz swobodnie przetestować o wiele więcej. Np. test, który będzie odpalał metodę w danej klasie, ale jednocześnie odwoływał się pod spodem do iluś różnych klas (nawet nie musi cię interesować, do jakich konkretnie). I to dalej może się w miarę szybko wykonywać.

  • Najwięcej unit testów, w drugiej kolejności testów integracyjnych, natomiast klikania ręcznie najmniej, jak to możliwe

Klikanie jest dalej potrzebne, tylko, że co innego testuje. Testy automatyczne nie wykryją ci, że coś brzydko wygląda na którejś z przeglądarek albo jest nieintuicyjne, nieużywalne. Dlatego w każdym projekcie z jakimś UI trzeba mieć kogoś, kto to przeklika. No chyba, że w przyszłości powstanie AI, która będzie obdarzona zmysłem estetycznym oraz będzie miała percepcję jak człowiek (również pod kątem ograniczeń, wad). Może wtedy będzie się dało wyeliminować całkowicie potrzebę klikania.

Unit testy nie chronią przed niezamierzonymi regresjami w obrębie metody / klasy, którą testują
Jeśli kod danej metody / klasy ulega zmianie, to zmianie musi często ulec także jej unit test. Zatem stary unit test jest wyrzucony i nic nie testuje

Jeśli po każdym commicie trzeba przepisać test, to oznacza, że testy zostały źle napisane. Dobre testy to takie, które przetrwają próbę czasu. A żeby to osiągnąć, muszą być trochę "czarnoskrzynkowe".

Zły test to taki, który sprawdza zbyt mocno szczegóły implementacji. Czyli masz silnię zaimplementowaną rekurencyjnie:

function factorial(n) {
    return n == 1? 1 : factorial(n - 1) * n;
}

Wtedy zły test mockuje funkcję factorial i sprawdza, czy została wywołana z argumentem n - 1. Problem tylko, że jak implementacja się zmieni (np. zamiast rekurencji zrobimy pętlę), to taki test jest do wyrzucenia. Więc lepiej nie testować takich szczegółów, tylko po prostu sprawdzać czy funkcja daje prawidłowe wartości (np. dla 4! ma zwrócić 24, dla 5! ma zwrócić 120 itp.). Wtedy implementacja może się zmienić, a test dalej będzie działał.

I to można zastosować również na większych kawałkach kodu. Zamiast sprawdzać, czy dana klasa działa i czy wywołuje inne (zamockowane) klasy w konkretny sposób, to można testować całą funkcjonalność - czyli odpalasz metodę w klasie A i nie interesuje cię, jakie klasy pomocnicze będzie ruszać po drodze (i nie musisz ich nawet mockować), ważne, żeby był zrobiony dany efekt.

Ogólnie pojęcie "unit testów" jest tak zajechane i źle rozumiane, że lepiej w ogóle o tym nie myśleć i wyrzucić słówko "unit", bo to robi więcej zamieszania niż pożytku. Ludzie się skupiają zbyt mocno na słowie "unit" (źle rozumianym na dodatek), a za mało na słowie "test".

9
YetAnohterone napisał(a):
  • Dlatego:
    • Mają testować tylko pojedynczą metodę, a już na pewno nic więcej, jak klasę
    • Wszystko inne ma być zamockowane

Tu masz błąd i to duży. Byłem w projekcie gdzie tak pisaliśmy testy bo nam nagle kazano mieć 80% coverage i sami nie widzieliśmy w tym sensu ani nie mieliśmy doświadczenia - unit tests to był wtedy jedynie buzzword i wszyscy wiedzieli że trzeba ale nikt nie widział jak. W większości przypadków nie było co testować bo klasa wywoływała tylko metody z innych klas. W teście wszystko było zamockowane więc co tu testować? Testowaliśmy np czy metoda została wywołana albo czy metody innych klas zostały wywołane w dobrej kolejności, albo czy metoda która zwraca wartość z mocka który został chwilę wcześniej ustawiony żeby zwrócił wartość x też zwraca wartość x - totalny bezsens i betonowanie kodu. Trzeba przyznać że napisaliśmy całkiem dobre testy... dla biblioteki mockującej.
W dodatku uzyskanie 80% coverage to była katorga bo trzeba było napisać test do każdej jednej metody, mnóstwo roboty której nikt nie widział sensu. Nigdy się nie zdarzyło żeby test wykazał regresję i tylko raz wykryliśmy błąd podczas pisania testów.

Gdy zacząłem testować jednostki / funkcje systemu, a nie klasy; z zasadą żeby mockować jak najmniej się da a nie wszystko to nagle się okazuje że jeden test potrafi stworzyć pokrycie 100% w kilku klasach na raz, nie zajmuje zbyt dużo czasu a w dodatku coś faktycznie testuje i nie betonuje kodu, za to tworzy barierę ochronną dla wymagań biznesowych o których czasem w przyszłości się zapomina i przypadkowo wyrzuca / zmienia kod który wydaje się być dziwnie / niepotrzebnie napisany a który był napisany w ten konkretny sposób żeby obsłużyć jakiś przypadek brzegowy. Pisząc test sprawiamy że później osoba która przejmie projekt od razu będzie miała czerwone testy jeśli o czymś nie pomyśli przy refaktoryzacji.
Dobre testy potrafią stanowić bardzo dobrą dokumentację

0

Polecam:

2

Jeśli mój stub - tracę dużo czasu, bo piszę tego mocka, a poza tym testy nie testują, czy mój program działa, bo ja mogę wcale nie rozumieć dziwactw tego 3rd party API.

Mitologia, napisanie takiego mocka to kilka linijek z jakimś wiremockiem. Nie musisz rozumieć żadnych dziwactw, musisz tylko wiedzieć ze na request X masz odpowiadać Y. Na dobrą sprawę są nawet toole które pozwalają "nagrać" taką interakcje za ciebie.

1
YetAnohterone napisał(a):

Załóżmy, że piszę program pobierający dane z jakiegoś 3rd party API. Czy uruchamianie testów powinno uderzać w to 3rd party API, czy też w stub tego 3rd party API, który dla celów testowania napiszę?

Jeśli 3rd party API, to testy nie działają, bo 3rd party API poszło w dół. Źle?

Bardzo źle. Dlatego to nie mogą być jedyne testy.

Jeśli mój stub - tracę dużo czasu, bo piszę tego mocka, a poza tym testy nie testują, czy mój program działa, bo ja mogę wcale nie rozumieć dziwactw tego 3rd party API. Więc chyba jednak powinny rzeczywiście uderzać w 3rd party API. Jakkolwiek jest to sprzeczne z tym, co "w internetach" czytałem nt. testów.

Więcej czasu stracisz na testowanie ręczne niż na pisanie mocka (abstrahując od tego, że to banał do automatycznego wygenerowania w wielu przypadkach).
Masz rację, że takie testy nie gwarantują, czy Twój program działa. Dlatego to nie mogą być jedyne testy.

Po prostu, potrzebujesz różnych testów do sprawdzania różnych rzeczy na różnych poziomach. A liczba i rodzaj testów każdego rodzaju zależą od tego, jaki rodzaj oprogramowania tworzysz.

Characteristics of a good unit test

  • Fast. It is not uncommon for mature projects to have thousands of unit tests. Unit tests should take very little time to run. Milliseconds.
  • Isolated. Unit tests are standalone, can be run in isolation, and have no dependencies on any outside factors such as a file system or database.
  • Repeatable. Running a unit test should be consistent with its results, that is, it always returns the same result if you do not change anything in between runs.

To wszystko prawda. Zauważ, że tam nie jest napisane, że to mają być jedyne testy. :)

LukeJL napisał(a):

Ogólnie pojęcie "unit testów" jest tak zajechane i źle rozumiane, że lepiej w ogóle o tym nie myśleć i wyrzucić słówko "unit", bo to robi więcej zamieszania niż pożytku. Ludzie się skupiają zbyt mocno na słowie "unit" (źle rozumianym na dodatek), a za mało na słowie "test".

Z tym się nie zgodzę. Wystarczy zastanowić się, albo dowiedzieć, co to słowo oznacza (za Merriam-Webster):

: a single thing, person, or group that is a constituent of a whole
: a piece or complex of apparatus serving to perform one particular function

i już wszystko jest jasne. Jeśli dana klasa nie jest w stanie samodzielnie niczego przydatnego zrobić, to nie jest jednostką.

obscurity napisał(a):

Gdy zacząłem testować jednostki / funkcje systemu, a nie klasy; z zasadą żeby mockować jak najmniej się da a nie wszystko to nagle się okazuje że jeden test potrafi stworzyć pokrycie 100% w kilku klasach na raz

100% pokrycia linijek kodu, nie tego, co kod robi.

5

Ja tylko dodam od siebie, że irytuje mnie w naszej branży przekonanie i "złota prawda", że unit testy są tanie - w swojej najpopularniejszej implementacji (czyli test per klasa) generują duży koszt przy refactoringu (i dodatkowy odłożony koszt, bo ludzie boją się zmieniać istniejący kod żeby nie zmieniać pierdyliarda unitów przez co idą na skróty)! Dlatego zgadzam się z postulatem, że testy powinny testować usecase'y aplikacji a to jak to robią (czy stawiają sobie external dependencje, czy wszystko in-memory) to już kwestia tradeoffów które podejmujemy i na które się godzimy (speed vs podobieństwo do środowiska produkcyjnego).

4
damianem napisał(a):

Ja tylko dodam od siebie, że irytuje mnie w naszej branży przekonanie i "złota prawda", że unit testy są tanie

Bo są. Test per klasa to nie jest unit test tylko aberracja.

1

Moim zdaniem testy powinny testować logikę biznesową, a nie kod.
tzn. na każdą funkcjonalność biznesową ma być test, a nie na każdą klasę/metodę/sztuczkę.

1

Mają testować tylko pojedynczą metodę, a już na pewno nic więcej, jak klasę

To jest najczęściej powtarzane kłamstwo w kontekście unit testów. Poza tym, jak masz kod z zachowanym SRP to nawet często 1 klasa = 1 (publiczna) metoda.

Wszystko inne ma być zamockowane

Nie, tylko zależności zewnętrzne. Jak mockujesz swoje klasy w testach to robisz to źle. Nawet można się pokusić o pisanie jakiś in-memory implementacji zależności do testów i wywalić bibliotekę do mockowania.

Najwięcej unit testów, w drugiej kolejności testów integracyjnych, natomiast klikania ręcznie najmniej, jak to możliwe

Jak aplikacja nie ma logiki do testowania to 0 unit testów będzie odpowiednie i parę integracyjnych.

Jeśli kod danej metody / klasy ulega zmianie, to zmianie musi często ulec także jej unit test.

Dzieje się to w takim samym stopniu dla każdego rodzaju testów, niezależnie czy to unity czy integracyjne czy e2e. Aczkolwiek kod powinien być pisany tak, żeby dostawienie ifa nie wywalało wszystkich testów :D

Unit testy nie chronią przed niezamierzonymi regresjami w innych miejscach kodu
Jak mają chronić, skoro wszystkie inne klasy są zamockowane?

To po co mockować skoro to tylko przeszkadza? Serio polecam spróbować wyrzucić bibliotekę mockującą z projektu i wszystko się samo upraszcza.

Refaktoryzacja unieważnia unit testy
Przeorganizowuję metody w klasie - muszę przepisać wszystkie / większość unit testów tych metod. Zmieniam publiczny interfejs klasy - muszę przepisać unit testy tego interfejsu. Stare testy są wyrzucone, więc nic nie testują i nie chronią przed regresjami w kodzie, który właśnie wprowadzam.

Głównym powodem pisania testów jednostkowych jest zabezpieczenie przed błędami powstałymi w wyniku refaktoryzacji :D
Tutaj powrót do punktu pierwszego. Testy jednostkowe nie mają testować metod tylko jednostki. Jednostką często będzie np. metodą w serwisie, która zwróci jakiś obiekt po pewnej bardziej skomplikowanej logice. A ta logika ta jest realizowana na wielu innych "wewnętrznych" bytach, czyli enkapsulowanych klasach, prywatnych metodach itd. (których nie testujesz jednostkowo!). Refactor tych wewnętrznych bytów nie zmienia w żaden sposób sygnatury metody z testowanego serwisu, więc jej testy pozostają niezmienne.

Możliwości są takie:

  • Albo uczysz się dopiero i wtedy skonfundowanie testami jednostkowymi jest częstym zjawiskiem, bo są pokazywane na trywialnych przykładach, np. sprawdź czy metoda add(2, 2) zwraca 4. Stąd też bierze się przekonanie, że unit test = test metody.
  • Albo widziałeś właśnie takie projekty z testami mockującymi wszystko. Jest to niestety częste zjawisko. To tak nie powinno wyglądać i wartość tego typu testów jest bliska zeru. Czasami wręcz przez upierdliwość w refactorowaniu okazuje się, że mają wartość ujemną, bo po refactoringu się 50 testów wywala i trzeba tracić czas na naprawianie.
  • Albo próbujesz pisać testy do CRUDa. Jeżeli funkcjonalność Twojej aplikacji polega w 90% na rozmawianiu z bazą danych to gdy zamockujesz bazę danych to surpise, surprise... Twoje testy są bez sensu. Unit testy nie mają sensu w aplikacjach CRUDowych, bo tam nie ma logiki. A unit testy chcesz mieć tam gdzie coś liczysz, zarządzasz stanem, poddajesz dane transformacjom itp.

No i co do piramidy testów to oczywiście zależy od typu aplikacji. Dla CRUDa "piramida testów" będzie wyglądać tak, że unit testów będzie bardzo mało, a integracyjnych dużo. Dla aplikacji commandline'owej obliczającej dowolne równanie matematyczne będzie bardzo dużo unit testów, a zero testów integracyjnych.

0

Dla mnie unit test to test, który mimo zmiany wymagań nie trzeba zmieniać / poprawiać.

By nie być zależym od wymagań to takimi testami nie testuję logiki aplikacji (bo tam to przeważnie są modele, procedury i interakcja z zewnętrznym api, które i tak będą z czasem się zmieniać).

Zamiast tego jednostkowo testuje rzeczy na których się opieram wzory, przekształcenia, które z jednej wartości produkują drugą, najlepiej jeśli są to jakieś zamknięte schematy, zasady na jakie nie ma wpływu biznes.

Natomiast jeśli test jest szybki i jeśli test opisuje jakąś regułę wymyśloną przez firmę to taki test można kodować, ale nie bardzo wiem po co. Jeśli firma schemat wymyśliła to sama może go zmienić i tak nie mam nad tym kontroli. Stąd takie testy apki wolę spychać dalej na testy, które za jednym zamachem obejmują większe pokrycie.

0

@fgh: Czyli jak firma sobie wymyśli, że jakaś wartość ma być sumą 2 elementów, to nie przetestujesz tego, bo za chwilę może zmienić zdanie i trzeba będzie zmienić np. na różnicę?

5
fgh napisał(a):

Dla mnie unit test to test, który mimo zmiany wymagań nie trzeba zmieniać / poprawiać.

To znaczy, że taki test niczego nie sprawdza, więc można go usunąć.

1

Dla mnie kłócenie się o to czy lepiej testować na poziomie klasy, "jednostki", integracyjnie, czy e2e jest trochę bez sensu, bo te testy mają inne zadania (przynajmniej moim skromnym zdaniem):

  • unit testy nie mają nic wspólnego z jakością aplikacji to co one naprawdę robią, to wymuszają pisanie przyzwoitego kodu i pozwalają na refaktoryzację tego co pokrywają, bez martwienia się, że cos przy okazji walnie. W dodatku jest to zwykle najprostsza metoda na udowodnienie, że jakiś kawałek kodu, który napisałem działa. Dla mnie najrozsądniejszym miejscem dodania takiego testu jest zewnętrzny interface jakiegoś modułu/pakietu. Jeżeli nie da się z tego poziomu pokryć całości kodu, to pojawia się pytanie co ten kod robi i czy aby na pewno nie mamy schowanych pod dywanem niejawnych zależności
  • testy integracyjne te moduły trzeba złożyć w większą całość i trzeba sprawdzić, czy ta całość robi to czego oczekujemy
  • e2e też warto zrobić, bo ostatecznie, co z tego, ze mamy super kod, którego każdy kawałek działa, ale ostatecznie nie robi tego czego oczekujemy. Tutaj nie rozumiem "piramidy testów" dla mnie przetestowanie w 100% funkcjonalności biznesowej, typu czy jak wystawimy fakturę, to zobowiązania klienta się zwiększą jest raczej podstawą. Można dyskutować, czy to ma być zaimplementowane w postaci testów automatycznych, czy skryptów dla testerów, ale trochę nie wyobrażam sobie oddawania klientowi systemu bez takich testów.
1

Jakiś czas temu napisałem artykuł o PODSTAWACH testów jednostkowych. Chciałbym do niego zaprosić @YetAnohterone . Być może coś mu rozjaśni to. Chętnie też posłucham, co inni sądzą na ten temat :)
https://masterbranch.pl/testy-jednostkowe-co-jak-i-po-co/

0

@slsy:

somekind napisał(a):
YetAnohterone napisał(a):
  • Dlatego:
    • Mają testować tylko pojedynczą metodę, a już na pewno nic więcej, jak klasę
    • Wszystko inne ma być zamockowane

To błędne myślenie. Testy jednostkowe mają testować jednostkę, nie klasę/metodę.

Co jeżeli CUT ma wszystkie zaleznosci zmockowane, piszemy nowa funkcjonalnosc i widzimy ze jakas metode fajnie by bylo wydzielic do innej, istniejacej klasy, zamiast stworzyc metode prywatna? Wydzielona metoda jest niezbedna czescia funkcjonalnosci (tej jednostki) wiec nie mozemy jej zmockowac, ale klasa do ktorej przenosimy metode juz jest zmockowana w innych testcasach. Co robicie w takich przypadkach, robicie settera na zaleznosci tylko po to, aby w miare potrzeby wstrzyknac mocka/prawdziwa implementacje?

0

@lambdadziara: dodaję nowy argument do konstruktora. Jak klasa ma się inaczej zachowywać (inaczej w testach, inaczej na produkcji) to nie da się tego zrobić (ładnie) inaczej, niż poprzez parametryzację.

Wydzielona metoda jest niezbedna czescia funkcjonalnosci (tej jednostki) wiec nie mozemy jej zmockowac, ale klasa do ktorej przenosimy metode juz jest zmockowana w innych testcasach.

Brzmi źle. Ogólnie klasy powinny być minimalistyczne. W tym przypadku masz dwa przypadki użycia i wychodzi na to, że w tym drugim (tam gdzie jest mock) ta metoda nie będzie w ogóle używana. W takiej sytuacji raczej nie używałbym mocka, bo wygląda to tak, że ciężko zamokować taki interfejs. Lepiej przemyśleć redesign albo użyć tej klasy bez zabawy w mocki.

2

jakas metode fajnie by bylo wydzielic do innej, istniejacej klasy, zamiast stworzyc metode prywatna? Wydzielona metoda jest niezbedna czescia funkcjonalnosci

@lambdadziara: to co piszesz się zwyczajnie wyklucza. Albo ta metoda jest integralną częścią tej klasy, albo innej klasy. Jeśli faktycznie ma sens ją przenieść, a ty faktycznie musisz pisać testy gdzie chcesz pozbyć się "zewnętrznych zależności" z danej klasy, to logicznym jest że zmockujesz sobie tutaj odpowiedzi tej przeniesionej metody.

Dla mnie błąd jest tu już na poziomie koncepcyjnym, kiedy próbujesz testować klasę, a nie jakiś element systemu.

2
Shalom napisał(a):

jakas metode fajnie by bylo wydzielic do innej, istniejacej klasy, zamiast stworzyc metode prywatna? Wydzielona metoda jest niezbedna czescia funkcjonalnosci

@lambdadziara: to co piszesz się zwyczajnie wyklucza. Albo ta metoda jest integralną częścią tej klasy, albo innej klasy. Jeśli faktycznie ma sens ją przenieść, a ty faktycznie musisz pisać testy gdzie chcesz pozbyć się "zewnętrznych zależności" z danej klasy, to logicznym jest że zmockujesz sobie tutaj odpowiedzi tej przeniesionej metody.

z testowaniem klas to prawda, ale prywatne metody to faktycznie tak jak gdzieś napisał Uncle Bob zazwyczaj ukryte klasy https://androidmess.com/testing/private-methods-hidden-devils/
bardzo często z paru prywatnych metod które operują na "wewnętrznych" danych można zrobić uogólnioną klasę która w dodatku okazuje się że ma sens

0

Dam przykład z firmy w której kiedyś pracowałem. Firma nie miała unit testów, a jedynie functional testy po postacią record-playbecku, które musiały robić za unit testy.
Wyglądało to następująco dla naszej aplikacji desktopowej i testowania funkcji "Jakaśtam":

(to jest lista, ale ta funkcjonalnośc została uszkodzona w edytorze:)
Wczytywaliśmy design testowy (nieważne czego) w trybie record-playback
Tworzyliśmy "nasze" elementy funkcją "EneDueLIkeRabe"
Odpalaliśmy "Jakaśtam"
Eksportowaliśmy design do txt

Uruchamiany przez skrypty test składał się z designu początkowego, odgrywanego record-playbacku i oczekiwanego pliku tekstowego.

Był to naprawdę test funkcjonalny, bo testował design początkowy w tym jego wczytywanie, tworzenie naszych elementów "EneDueLikeRabe", właściwą funkcjonalność "Jakaśtam" i eksport do tekstu, Ale robił za unit test "Jakaśtam".

Problem polegał na tym, że jak ktoś przepisał tworzenie "naszych" elementów z "EneDueLIkeRabe" na "LikeRabeEneDue" to choć funkcjonalnie nic się nie zmieniło, to kolejność elementów w wynikowym pliku tekstowym uległa zmianie i trzeba było ponagrywać większość testów. Co oznacza, że część prawidłowo wskazanych przez testy błędów, np. w nieprawidłowym działaniu "Jakaśtam" po zmianach, mogła być przeoczona i nagrana ponownie jako prawidłowe zachowanie!

Gdyby w firmie istniały prawdziwe unit testy, takie wywołujące tylko "Jakaśtam" ze zmockowanym wejściem i sprawdzała wyjście, to zmiany w niepowiązanych funkcjonalnościach nie miałyby wpływu na wynik testu. A jeśli by się zmienił, to byłoby wiadomo, że mamy regresję bo funkcjonalność jednak była powiązana.

1

@Wyjątek mam wrażenie że co prawda widzisz ze był problem, ale nie jestem pewien czy dobrze zdiagnozowałeś przyczynę. Dla mnie błąd był w tym co sprawdzały asercje, tzn ze na przykład sprawdzały kolejność w pliku mimo że ta kolejność wcale nie stanowiła wymagania. I nie ma tu znaczenia czy to był unit test czy to był taki fixture test jaki opisałeś. Unitami z mockami można zrobić identyczne bagno, jak ktoś weźmie się za weryfikacje kolejności i liczby wywołań metod na mockach ;)

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