Nie lubię testować prywatnej funkcjonalności z kilku powodów. Są one następujące (są to główne punkty dla osób TLDR):
- Zazwyczaj, gdy masz ochotę przetestować prywatną metodę klasy, jest to zapach projektowy.
- Możesz je przetestować za pomocą publicznego interfejsu (w taki sposób chcesz je przetestować, ponieważ w ten sposób klient będzie je wywoływał / używał). Możesz uzyskać fałszywe poczucie bezpieczeństwa, widząc zielone światło na wszystkich pozytywnych testach dla twoich prywatnych metod. O wiele lepiej / bezpieczniej jest testować najnowsze przypadki prywatnych funkcji za pośrednictwem publicznego interfejsu.
- Ryzykujesz poważne powielanie testów (testy, które wyglądają / czują się bardzo podobnie), testując prywatne metody. Ma to poważne konsekwencje, gdy zmienią się wymagania, ponieważ przerwie się o wiele więcej testów niż to konieczne. Może także postawić cię w sytuacji, w której trudno jest dokonać refaktoryzacji z powodu twojego zestawu testowego ... co jest ostateczną ironią, ponieważ zestaw testowy jest po to, aby pomóc ci bezpiecznie przeprojektować i refaktoryzować!
Wyjaśnię każdy z nich konkretnym przykładem. Okazuje się, że 2) i 3) są dość ściśle powiązane, więc ich przykład jest podobny, chociaż uważam je za osobne powody, dla których nie powinieneś testować metod prywatnych.
Są chwile, w których testowanie prywatnych metod jest odpowiednie, ważne jest, aby zdawać sobie sprawę z wyżej wymienionych wad. Omówię to później bardziej szczegółowo.
Zastanawiam się również, dlaczego TDD nie jest usprawiedliwieniem do testowania prywatnych metod na samym końcu.
Refaktoryzacja wyjścia ze złego projektu
Jednym z najczęstszych (anty) paternów, które widzę, jest to, co Michael Feathers nazywa klasą „Góry lodowej” (jeśli nie wiesz, kim jest Michael Feathers, idź kupić / przeczytaj jego książkę „Skutecznie współpracując ze Starszym Kodem”. osoba, o której warto wiedzieć, czy jesteś profesjonalnym inżynierem / programistą). Istnieją inne (anty) wzorce, które powodują pojawienie się tego problemu, ale jest to zdecydowanie najbardziej powszechny, z jakim się zetknąłem. Klasy „Góra lodowa” mają jedną metodę publiczną, a pozostałe są prywatne (dlatego kuszące jest przetestowanie metod prywatnych). Nazywa się to klasą „Góra lodowa”, ponieważ zwykle występuje samotna metoda publiczna, ale reszta funkcji jest ukryta pod wodą w postaci prywatnych metod.
Na przykład możesz przetestować GetNextToken()
, wywołując go kolejno i sprawdzając, czy zwraca oczekiwany wynik. Funkcja taka jak ta gwarantuje test: takie zachowanie nie jest trywialne, szczególnie jeśli twoje reguły tokenizacji są złożone. Udawajmy, że to nie jest aż tak skomplikowane, a my chcemy po prostu wplatać tokeny oddzielone spacją. Więc piszesz test, może wygląda to tak (jakiś agnostyczny kod psuedo, mam nadzieję, że pomysł jest jasny):
TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
re = RuleEvaluator(input_string);
ASSERT re.GetNextToken() IS "1";
ASSERT re.GetNextToken() IS "2";
ASSERT re.GetNextToken() IS "test";
ASSERT re.GetNextToken() IS "bar";
ASSERT re.HasMoreTokens() IS FALSE;
}
Cóż, to naprawdę ładnie wygląda. Chcemy mieć pewność, że utrzymamy to zachowanie podczas wprowadzania zmian. Ale GetNextToken()
jest funkcją prywatną ! Nie możemy więc przetestować tego w ten sposób, ponieważ nawet się nie skompiluje (zakładając, że używamy języka, który faktycznie wymusza publiczny / prywatny, w przeciwieństwie do niektórych języków skryptowych, takich jak Python). Ale co ze zmianą RuleEvaluator
klasy, aby postępowała zgodnie z zasadą pojedynczej odpowiedzialności (zasada pojedynczej odpowiedzialności)? Na przykład wydaje się, że analizator składni, tokenizer i ewaluator są zablokowane w jednej klasie. Czy nie lepiej byłoby po prostu rozdzielić te obowiązki? Ponadto, jeśli utworzysz Tokenizer
klasę, wówczas będą to metody publiczne HasMoreTokens()
i GetNextTokens()
. RuleEvaluator
Klasa mogłaby miećTokenizer
obiekt jako członek. Teraz możemy zachować taki sam test jak powyżej, z tym wyjątkiem, że testujemy Tokenizer
klasę zamiast RuleEvaluator
klasy.
Oto, jak może to wyglądać w UML:
Zauważ, że ten nowy projekt zwiększa modułowość, więc możesz potencjalnie ponownie użyć tych klas w innych częściach twojego systemu (zanim nie mogłeś, metody prywatne nie są z definicji wielokrotnego użytku). Jest to główna zaleta zepsucia RuleEvaluator wraz ze zwiększoną zrozumiałością / lokalizacją.
Test wyglądałby bardzo podobnie, tyle że faktycznie skompilowałby się tym razem, ponieważ GetNextToken()
metoda jest teraz publicznie dostępna w Tokenizer
klasie:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS FALSE;
}
Testowanie komponentów prywatnych za pomocą publicznego interfejsu i unikanie powielania testów
Nawet jeśli nie uważasz, że możesz podzielić swój problem na mniejszą liczbę komponentów modułowych (co możesz zrobić w 95% przypadków, jeśli tylko spróbujesz to zrobić), możesz po prostu przetestować funkcje prywatne za pośrednictwem publicznego interfejsu. Często członkowie prywatni nie są warci testowania, ponieważ będą testowani przez interfejs publiczny. Często widzę testy, które wyglądają bardzo podobnie, ale testują dwie różne funkcje / metody. Ostatecznie dzieje się tak, że gdy wymagania się zmieniają (i zawsze tak się dzieje), masz teraz 2 zepsute testy zamiast 1. A jeśli naprawdę przetestowałeś wszystkie swoje prywatne metody, możesz mieć więcej takich jak 10 zepsutych testów zamiast 1. W skrócie , testowanie funkcji prywatnych (przy użyciuFRIEND_TEST
lub upublicznienie ich lub użycie refleksji), które w innym przypadku mogłyby zostać przetestowane przez interfejs publiczny, mogą spowodować powielenie testu . Naprawdę tego nie chcesz, ponieważ nic nie boli bardziej niż zestaw testów, który cię spowalnia. Ma skrócić czas opracowywania i koszty konserwacji! W przypadku testowania metod prywatnych, które w innym przypadku byłyby testowane za pomocą interfejsu publicznego, zestaw testów może równie dobrze zrobić coś przeciwnego i aktywnie zwiększyć koszty konserwacji i wydłużyć czas programowania. Kiedy upubliczniasz funkcję prywatną lub jeśli używasz czegoś takiego FRIEND_TEST
i / lub refleksji, zwykle na dłuższą metę zwykle będziesz żałować.
Rozważ następującą możliwą implementację Tokenizer
klasy:
Powiedzmy, że SplitUpByDelimiter()
odpowiada za zwrócenie tablicy tak, aby każdy element w tablicy był tokenem. Co więcej, powiedzmy, że GetNextToken()
jest to po prostu iterator tego wektora. Twój publiczny test może wyglądać następująco:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS false;
}
Udawajmy, że mamy narzędzie, którego nazywa Michael Feather . To narzędzie pozwala dotykać części prywatnych innych osób. Przykład pochodzi FRIEND_TEST
z googletestu lub refleksji, jeśli język go obsługuje.
TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
result_array = tokenizer.SplitUpByDelimiter(" ");
ASSERT result.size() IS 4;
ASSERT result[0] IS "1";
ASSERT result[1] IS "2";
ASSERT result[2] IS "test";
ASSERT result[3] IS "bar";
}
Powiedzmy teraz, że wymagania się zmieniają, a tokenizacja staje się znacznie bardziej złożona. Decydujesz, że prosty ogranicznik łańcucha nie wystarczy i potrzebujesz Delimiter
klasy do obsługi zadania. Oczywiście, będziesz oczekiwać, że jeden test się zepsuje, ale ten ból nasila się, gdy testujesz funkcje prywatne.
Kiedy testowanie metod prywatnych może być właściwe?
W oprogramowaniu nie ma „jednego rozmiaru dla wszystkich”. Czasami w porządku (i właściwie idealnym) jest „łamanie zasad”. Zdecydowanie opowiadam się za nie testowaniem prywatnej funkcjonalności, kiedy możesz. Są dwie główne sytuacje, kiedy myślę, że jest w porządku:
Pracowałem intensywnie ze starszymi systemami (dlatego jestem wielkim fanem Michaela Feathersa) i mogę śmiało powiedzieć, że czasami najbezpieczniej jest po prostu przetestować prywatną funkcjonalność. Może to być szczególnie pomocne przy wprowadzaniu „testów charakteryzacyjnych” do linii podstawowej.
Spieszysz się i musisz zrobić najszybszą możliwą rzecz tu i teraz. Na dłuższą metę nie chcesz testować prywatnych metod. Powiem jednak, że refaktoryzacja zazwyczaj zajmuje trochę czasu, aby rozwiązać problemy projektowe. A czasem musisz wysłać za tydzień. W porządku: zrób szybki i brudny test i przetestuj prywatne metody za pomocą narzędzia do omijania, jeśli uważasz, że jest to najszybszy i najbardziej niezawodny sposób na wykonanie zadania. Ale zrozumcie, że to, co zrobiliście, nie było optymalne na dłuższą metę, i proszę rozważyć powrót do tego (lub, jeśli zostało zapomniane, ale zobaczycie to później, naprawcie to).
Prawdopodobnie są inne sytuacje, w których jest w porządku. Jeśli uważasz, że to w porządku i masz dobre uzasadnienie, zrób to. Nikt cię nie powstrzymuje. Pamiętaj tylko o potencjalnych kosztach.
Wymówka TDD
Nawiasem mówiąc, naprawdę nie lubię ludzi używających TDD jako wymówki do testowania prywatnych metod. Ćwiczę TDD i nie sądzę, żeby TDD zmusiło cię do tego. Możesz najpierw napisać test (dla interfejsu publicznego), a następnie napisać kod, aby spełnić ten interfejs. Czasami piszę test interfejsu publicznego i spełnię go, pisząc jedną lub dwie mniejsze metody prywatne (ale nie testuję metod prywatnych bezpośrednio, ale wiem, że działają lub mój test publiczny nie powiedzie się ). Jeśli będę musiał przetestować przypadki tej prywatnej metody, napiszę całą masę testów, które trafią je za pośrednictwem mojego publicznego interfejsu.Jeśli nie możesz dowiedzieć się, jak trafić na skrajne skrzynie, jest to mocny znak, że musisz przeforsować małe komponenty za pomocą własnych metod publicznych. To znak, że funkcje prywatne robisz za dużo i poza zakresem klasy .
Czasami też zdarza mi się, że piszę test, który jest w tej chwili zbyt duży, by go przeżuć, i dlatego myślę: „eh wrócę do tego testu później, gdy będę miał więcej interfejsu API do pracy” (I Skomentuję to i zachowam w pamięci). W tym miejscu wielu deweloperów, których spotkałem, zacznie pisać testy ich prywatnej funkcjonalności, używając TDD jako kozła ofiarnego. Mówią „och, no cóż, potrzebuję innego testu, ale aby napisać ten test, będę potrzebować tych prywatnych metod. Dlatego, ponieważ nie mogę napisać żadnego kodu produkcyjnego bez napisania testu, muszę napisać test dla metody prywatnej ”. Ale to, co naprawdę muszą zrobić, to przefakturowanie na mniejsze komponenty wielokrotnego użytku zamiast dodawania / testowania szeregu prywatnych metod do ich obecnej klasy.
Uwaga:
Niedawno odpowiedziałem na podobne pytanie dotyczące testowania metod prywatnych za pomocą GoogleTest . Przeważnie zmodyfikowałem tę odpowiedź, aby była bardziej niezależna od języka.
PS Oto odpowiedni wykład na temat zajęć z góry lodowej i narzędzi do omijania autorstwa Michaela Feathersa: https://www.youtube.com/watch?v=4cVZvoFGJTU