Co do pisania testów przed implementacją, to ja mogę podać kilka argumentów przeciw:
-
Tylko w idealnym świecie oraz książkach interfejs jest całkowicie niezależny od implementacji. W praktyce jednak implementacja ma wpływ na interfejs, a interfejs na implementację. Myśląc tylko o samym interfejsie, możemy zaprojektować taki interfejs, że implementacja będzie 3x bardziej skomplikowana niż mogłaby być. Możemy też spowodować problemy z wydajnością - np. konieczność przekształcania formatu danych do takiego jaki zażyczył sobie projektant interfejsu, albo problemy na styku sync/async. W efekcie pisząc testy pod interfejs zaprojektowany w oderwaniu od implementacji może się okazać, że w trakcie pisania implementacji będziemy musieli zmienić interfejs, a w efekcie przepisać testy. Innymi słowy - marnujemy czas na przepisywanie testów kilka razy.
-
Pisanie implementacji pod gotowy zestaw testów powoduje u niektórych programistów chęć "zaliczenia" wszytkich testów byle jak, byle tylko na końcu było zielone. W efekcie czytelność i prostota implementacji pozostawia wiele do życzenia. Np. widziałem kod ogólnie całkiego dobrego programisty, który folgując TDD, napisał implementację odzwierciedlającą po prostu po kolei każdy przypadek testowy jakimś wielkim switchem czy drabinką ifów, zamiast uogólnić wszystkie przypadki. A ponieważ w testach brakowało dwóch szczególnych przypadków, a jeden był na dodatek źle, to i kod miał analogiczne błędy.
-
Testy przed implementacją wymuszają programowanie/projektowanie stylem top-down. W efekcie można spędzić 5 dni na pisanie kodu i dalej nie mieć nic działającego, prócz kilku pięknych warstw abstrakcji. Ja tu widzę pewien paradoks, bo top-down jest bardzo, bardzo "nie-agile". Tymczasem wielu programistów, których podziwiam, i którzy piszą bardzo dobrze zaprojektowany kod, preferują bottom-up (np. Paul Graham, Peter Norvig, Ken Thompson, Linus Torvalds).
http://www.paulgraham.com/progbot.html
Pisząc testy przed łatwiej je napisać skrupulatnie, zastanawiając się, co metoda ma przyjąć, i co zwrócić. Takie testy testują sensowne przypadki testowe, błędne dane i warunki brzegowe
Ten argument raczej działa na korzyść pisania testów po, a nie przed. Nie znając implementacji, nie jesteś w stanie określić prawidłowo wszystkich przypadków brzegowych, bo mogą być przypadki brzegowe ukryte, wynikające z implementacji. Przypadkiem brzegowym mogą być np. dane wejściowe o wielkości dokładnie 65536 B, bo implementacja używa bufora o takiej dokładnie wielkości i pechowo tylko dla tej wartości jest off-by-one error... Pisząc testy przed implementacją nie masz szans takich rzeczy sprawdzić.
Pisanie testów "po" zazwyczaj kończy się przetestowaniem jedynie kilku optymistycznych ścieżek. Takie testy są nic nie warte.
Test testujący kilka optymistycznych ścieżek może nie jest kompletny, ale też ma pewną wartość. Zwłaszcza jeśli te optymistyczne ścieżki to akurat te, które są wykorzystywane przez klientów. Taki test nadal chroni przed regresjami.
- trzeba mieć minimum XX% pokrycia testami
Tu jestem skonfundowany. W poście piszesz takie rzeczy, a w komentarzach się ze mną kłócisz, gdy piszę, że ta miara jest bez sensu.
Sama miara nie jest bez sensu. Natomiast bez sensu jest dogmatyczne używanie jej jako jedynego wyznacznika jakości testów. Pokrycie linijkowe jest użyteczne w celu pokazania, które fragmenty kodu wymagają jeszcze testów. Ale nie jest użyteczne określenie, że ma być pokrycie 80% i ani pół procenta mniej.
Nie dowodzą w sensie matematycznym, bo i nie ma sensu takiego dowodzenia w praktyczniej dziedzinie.
Statyczny system typów dowodzi braku błędów (pewnej klasy) dla wszystkich możliwych wejść, nawet jeśli liczba ta jest nieskończona, w sensie matematycznym. Dysponując silnym systemem typów mogę pisać tak kod, aby zminimalizować zapotrzebowanie na testy. Stąd testy powinno się robić po, aby pokryć tylko te przypdadki, których nie dało się uwzględnić w systemie typów / analizą statyczną. Na przykład, jeśli system typów potrafi udowodnić, że dana metoda nigdy nie zwraca null, to nie ma sensu pisać testów, które to testują - będzie to marnowanie czasu. Oczywiście w praktyce system typów nie potrafi udowodnić wszystkiego, więc testy są nadal użyteczne.
Jeśli pisze się testy "po", często kończy się to klasami testowymi będącymi ogromnymi spaghetti godobjectami, bo są one tylko "dodatkiem" do aplikacji, a nie metodą jej wytwarzania.
Testy są dodatkiem do aplikacji, a nie są metodą jej wytwarzania. Testy to też jest kod, który trzeba wytworzyć i utrzymać. Im mniej, aby osiągnąć cel, tym lepiej. Testy są użytecznym narzędziem, ale są tylko jednym z wielu narzędzi zapewnienia odpowiedniej jakości projektu. Robienie z nich centralnego elementu wytwarzania oprogramowania to jakieś nieporozumienie. Klienta nie obchodzą testy, klienta obchodzi działające oprogramowanie.