Pisząc unit testy chcielibyśmy wiedzieć, czy robimy to wystarczająco dobrze i czy dodajemy w ten sposób wartość do projektu. Informacja ta jest potrzebna programistom, aby mogli doskonalić swój warsztat i ułatwiać pracę zespołowi. Korzystają z niej również managerowie planując zadania, skład zespołu itp. Najczęściej wykorzystywaną metryką jest tutaj test coverage, jednak niesie ona jedynie ograniczoną informację. Ważne są również miary empiryczne, które ciężko przedstawić w formie liczbowej.
Jakie czynniki świadczą o jakości testów?
Na początek zastanówmy się, jakie warunki muszą spełniać dobre testy. Nie mówię tutaj o zasadach pisania pojedynczych testów. Ten temat poruszałem w jednym z wcześniejszych wpisów.
Tym razem chodzi mi o warunki spełniane przez testy jako całość. Przede wszystkim testy powinny sprawdzać, czy kod działa tak, jak od niego oczekujemy. Musimy więc sprawdzić całą logikę aplikacji. Z drugiej strony jednak testów powinno być jak najmniej, żeby łatwo było je modyfikować i żeby nie sprawdzały tego samego. Testy powinny również wykrywać błędy i ułatwiać wprowadzanie zmian. Mamy więc cztery zadania:
- Sprawdzenie całej logiki aplikacji.
- Brak nadmiarowych testów.
- Znajdowanie błędów.
- Ułatwienie wprowadzania zmian.
Od razu widzimy, że nie wszystkie z tych punktów da się opisać metrykami liczbowymi.
Test coverage
Pokrycie kodu testami (po angielsku code coverage, test coverage) to stosunek kodu produkcyjnego wykonywanego podczas uruchamiania testów do całości kodu produkcyjnego. Istnieją różne rodzaje code coverage, ale dla wszystkich wartość podawana jest w procentach – 100% oznacza, że każda linia kodu jest pokryta testami. Rodzaje coverage to temat na osobny tekst, dlatego nie będę go teraz rozwijać.
Test coverage sprowadza informację o testach do jednej wartości od 0 do 100%. Możemy również sprawdzić coverage oddzielnie dla poszczególnych klas, funkcji, bloków czy plików. Ze względu na prostotę metryka ta jest wykorzystywana w wielu projektach. Niestety często błędnie. Częstą praktyką jest określanie minimalnej dozwolonej wartości i dążenie do przekroczenia jej za wszelką cenę. Innym błędem jest pogoń na siłę za 100% pokrycia.
Miara pokrycia kodu jest jedynie uproszczoną informacją. Jeżeli jest zbyt niska, mówi nam, że testujemy niewystarczająco dokładnie. Możemy za jej pomocą zidentyfikować obszary wymagające poprawy testów. Jednak wysoka wartość wcale nie musi oznaczać, że testy są napisane dobrze. Test może na przykład wykonywać kod bez assertów (antywzorzec Kłamca). Dlatego naszym głównym celem powinno być tworzenie przydatnych i dobrze napisanych testów, a nie nabijanie coverage. Wysoka wartość test coverage to tylko efekt uboczny.
Proporcje kodu produkcyjnego i testowego
Inną czasem używaną metryką opisującą unit testy jest ilość linii kodu testowego w stosunku do produkcyjnego. Jest ona wykorzystywana na przykład do chwalenia się: „U nas na każdą linijkę kodu produkcyjnego przypada linijka testów”. Prawda jest jednak taka, że ta metryka nie niesie żadnej wartości. Ilość linii jest zależna od wykorzystywanego języka, frameworków i od stylu pisania. Dobrze obrazuje to przykładowy kod:
double Foo(void) { return 2.0; } //4 lines of production code void testFoo(void) { assert(2.0 == Foo()); } //4 lines of test code - ratio 1:1, 50%:50% void testFoo(void) { double expected; double actual; expected = 2.0; actual== Foo(); assert (expected == actual); } //8 lines of test code - ratio 1:2, 33%:66%
Jedyne, czego możemy się w ten sposób dowiedzieć, to że wpadamy w jakąś skrajność. Jeśli mamy wiele razy więcej kodu produkcyjnego, albo testów np. 30:1 – coś jest pewnie nie tak. Jednak to są tak oczywiste przypadki, że widzimy to nawet bez liczenia. Polecam dyskusję na ten temat na wiki c2.
Miary empiryczne
Unit testy piszemy po to, aby odnieść konkretne korzyści. Najważniejsze to:
- Mniejsza ilość błędów w wersjach produkcyjnych.
- Łatwość wprowadzania zmian bez strachu o zepsucie innego fragmentu kodu
Możemy zbierać statystyki na ten temat za pomocą issue trackera, jednak nie zawsze mamy pełny obraz. W różnych fazach developmentu powstaje różna ilość defektów, poza tym możemy nie mieć danych porównawczych bez testów. Zbieranie takich statystyk wymaga również czasu. Nie dysponując twardymi danymi możemy zawsze poznać opinię osób pracujących nad projektem. Łatwość wykrywania błędów i wprowadzania zmian odbija się pozytywnie na jakości pracy programisty dużo szybciej, niż widać to w metrykach.
Istnienie nadmiarowych testów również najłatwiej wykryć po prostu pracując z kodem. Jeżeli testy sprawdzają wielokrotnie te same warunki, albo są zbyt szczegółowe – od razu odbije się to na szybkości wprowadzania zmian w kodzie. Każda zmiana będzie się wiązała z poprawieniem wielu scenariuszy testowych.
Podsumowanie
Najważniejszym wskaźnikiem jakości testów jest ilość błędów przedostających się na produkcję. Nie jesteśmy w stanie tego łatwo zmierzyć, możemy jedynie stosować analizę danych historycznych. Programiści odczują skutki dobrze lub źle napisanych testów jeszcze zanim znajdą one odzwierciedlenie w liczbach. Code coverage również warto monitorować, aby wiedzieć, czy nie zapomnieliśmy pokryć testami jakiś bloków. Jednak trzymanie się sztywno z góry ustalonego progu pokrycia nie jest dobrym rozwiązaniem.
Dodatkowe źródła
https://martinfowler.com/bliki/TestCoverage.html
9 marca 2019 at 16:12
Dla mnie najważniejsze jest to:
„Łatwość wprowadzania zmian bez strachu o zepsucie innego fragmentu kodu”
już się spotkałem, że „jedna mała poprawka” w bibliotece współdzielonej spowodowała wywalenie się innego projektu. Co przerodziło się w podejście „jeśli musisz zmodyfikować bibliotekę współdzieloną to lepiej ją skopiuj lokalnie, zmiany kiedyś zmergujemy” (czyt. raczej nie zrobimy tego, bo to zbyt ryzykowne). Nie wspomne o zamieszaniu jakie się zrobi jakby kilka osób miało jakieś lokalne poprawki – nawet możliwe, ze dotyczące tego samego.
Testy drastycznie zmniejszają to ryzyko. Do o czym wspomniałeś -> ktoś coś poprawia to dopisuje test na bazie tego co znalazł, że nie działa poprawnie, poprawia i testuje, czy już działa + ma pewność, że wszystko dookoła się nie zepsuło.