Uwagi wstępne
Nie będę się rozróżniał różnych rodzajów testów, na tych stronach jest już kilka pytań na ten temat.
Wezmę to, co tam jest i co mówi: testowanie jednostkowe w sensie „testowanie najmniejszej możliwej do wydzielenia jednostki aplikacji”, z której faktycznie pochodzi to pytanie
Problem izolacji
Jaka jest najmniejsza możliwa do wydzielenia jednostka programu. Cóż, jak widzę, to (wysoce?) Zależy od tego, w jakim języku kodujesz.
Micheal Feathers mówi o koncepcji szwu : [WEwLC, s. 31]
Szew to miejsce, w którym możesz zmienić zachowanie w swoim programie bez edycji w tym miejscu.
I bez wchodzenia w szczegóły, rozumiem szew - w kontekście testów jednostkowych - jako miejsce w programie, w którym „test” może łączyć się z „jednostką”.
Przykłady
Test jednostkowy - szczególnie w C ++ - wymaga od testowanego kodu dodania kolejnych szwów, które byłyby ściśle wymagane dla danego problemu.
Przykład:
- Dodanie interfejsu wirtualnego, w którym wystarczyłoby wdrożenie inne niż wirtualne
- Dzielenie - uogólnianie (?) - (mała) klasa dalej „tylko” w celu ułatwienia dodania testu.
- Dzielenie jednego pliku wykonywalnego na pozornie „niezależne” biblioteki lib, „tylko” w celu ułatwienia samodzielnej kompilacji ich na potrzeby testów.
Pytanie
Wypróbuję kilka wersji, które, mam nadzieję, zapytają o ten sam punkt:
- Jest to sposób, w jaki testy jednostkowe wymagają, aby struktura kodu aplikacji „tylko” była korzystna dla testów jednostkowych, czy faktycznie jest korzystna dla struktury aplikacji.
- Czy uogólnienie kodu, które jest potrzebne, aby można go było testować jednostkowo, jest przydatne do wszystkiego oprócz testów jednostkowych?
- Czy dodanie testów jednostkowych zmusza do niepotrzebnego uogólnienia?
- Czy testy jednostek kształtu wymuszają na kodzie „zawsze” również dobry ogólny kod, patrząc z dziedziny problemowej?
Pamiętam ogólną zasadę, która mówi: nie generalizuj, dopóki nie będziesz musiał / dopóki nie będzie drugiego miejsca, w którym użyjesz kodu. W przypadku testów jednostkowych zawsze jest drugie miejsce, które korzysta z kodu - mianowicie test jednostkowy. Czy to wystarczający powód do uogólnienia?
źródło
Odpowiedzi:
Tylko jeśli nie rozważasz przetestowania integralnej części rozwiązywania problemów. W przypadku każdego nietrywialnego problemu powinno tak być nie tylko w świecie oprogramowania.
W świecie sprzętu nauczyliśmy się tego już dawno - na poważnie. Producenci różnych urządzeń na przestrzeni wieków nauczyli się od niezliczonych spadających mostów, eksplodujących samochodów, palących procesory itp., Czego uczymy się teraz w świecie oprogramowania. Wszystkie wbudowują „dodatkowe szwy” w swoich produktach, aby umożliwić ich testowanie. Większość nowych samochodów ma obecnie porty diagnostyczne dla serwisantów, aby uzyskać dane o tym, co dzieje się w silniku. Znaczna część tranzystorów na każdym procesorze służy do celów diagnostycznych. W świecie sprzętu każde „dodatkowe” rzeczy kosztują, a gdy produkt jest wytwarzany przez miliony, koszty te z pewnością sumują się do dużych sum pieniędzy. Mimo to producenci są gotowi wydać wszystkie te pieniądze na testowanie.
Po powrocie do świata oprogramowania C ++ jest rzeczywiście trudniejszy do przetestowania w jednostce niż późniejsze języki z dynamicznym ładowaniem klas, refleksją itp. Mimo to większość problemów można przynajmniej złagodzić. W jednym projekcie C ++, w którym do tej pory korzystałem z testów jednostkowych, nie przeprowadzaliśmy testów tak często, jak np. W projekcie Java - ale nadal były one częścią naszej kompilacji CI i uznaliśmy je za przydatne.
Z mojego doświadczenia wynika, że ogólnie testowalna konstrukcja jest korzystna, nie „tylko” dla samych testów jednostkowych. Korzyści te występują na różnych poziomach:
Jeśli możesz udowodnić, że twoje oprogramowanie wykonuje dokładnie to, co powinno - i udowodnić to w szybki, powtarzalny, tani i wystarczająco deterministyczny sposób, aby zadowolić klientów - bez „dodatkowego” uogólnienia lub szwów wymuszonych przez testy jednostkowe, to idź (i daj nam znać, jak to robisz, ponieważ jestem pewien, że wiele osób na tym forum byłoby równie zainteresowanych jak ja :-)
Przy okazji zakładam, że przez „uogólnienie” masz na myśli wprowadzenie interfejsu (klasa abstrakcyjna) i polimorfizmu zamiast jednej konkretnej klasy - jeśli nie, wyjaśnij to.
źródło
Mam zamiar rzucić w ciebie The Way of Testivus , ale podsumowując:
Jeśli spędzasz dużo czasu i energii, komplikując kod w celu przetestowania pojedynczej części systemu, może to oznaczać, że twoja struktura jest nieprawidłowa lub że twoje podejście testowe jest błędne.
Najprostszy przewodnik jest następujący: testowany jest publiczny interfejs twojego kodu w sposób, w jaki ma on być używany przez inne części systemu.
Jeśli twoje testy stają się długie i skomplikowane, oznacza to, że korzystanie z publicznego interfejsu będzie trudne.
Jeśli musisz użyć dziedziczenia, aby umożliwić korzystanie z klasy przez cokolwiek innego niż pojedynczą instancję, do której ma być obecnie używana, istnieje duża szansa, że twoja klasa jest zbyt mocno związana ze środowiskiem użytkowania. Czy możesz podać przykład sytuacji, w której jest to prawda?
Uważaj jednak na dogmat o testowaniu jednostkowym. Napisz test, który pozwoli ci wykryć problem, który spowoduje, że klient krzyczy na ciebie .
źródło
TDD i testy jednostkowe są dobre dla całego programu, a nie tylko dla testów jednostkowych. Powodem tego jest to, że jest to dobre dla mózgu.
To jest prezentacja na temat konkretnej struktury ActionScript o nazwie RobotLegs. Jeśli jednak przejrzysz około 10 pierwszych slajdów, zaczniesz docierać do dobrych części mózgu.
Testy TDD i testów jednostkowych zmuszają cię do zachowania się w sposób, który lepiej mózg przetwarza i zapamiętuje informacje. Tak więc, podczas gdy Twoim dokładnym zadaniem przed tobą jest po prostu ulepszenie testu jednostkowego lub uczynienie kodu bardziej testowalnym w jednostce ... to, co faktycznie robi, sprawia, że kod jest bardziej czytelny, a tym samym łatwiejszy do utrzymania w kodzie. To przyspiesza kodowanie nawyków i pozwala szybciej zrozumieć kod, gdy trzeba dodać / usunąć funkcje, naprawić błędy lub ogólnie otworzyć plik źródłowy.
źródło
to prawda, ale jeśli posuniesz się za daleko, nie da ci to wiele i kosztuje dużo, i uważam, że to właśnie ten aspekt promuje użycie terminu BDD jako tego, czym powinno być TDD wzdłuż - najmniejsza możliwa do wydzielenia jednostka jest tym, czym chcesz.
Na przykład raz debugowałem klasę sieci, która miała (między innymi bitami) 2 metody: 1, aby ustawić adres IP, inną, aby ustawić numer portu. Oczywiście były to bardzo proste metody i z łatwością przeszłyby najbardziej trywialny test, ale jeśli ustawisz numer portu, a następnie ustawisz adres IP, nie zadziała - seter ip nadpisuje numer portu domyślnym. Musiałeś więc przetestować całą klasę, aby upewnić się, że zachowuje się poprawnie, coś, co myślę, że brakuje koncepcji TDD, ale BDD ci to daje. Naprawdę nie musisz testować każdej małej metody, gdy możesz przetestować najbardziej sensowny i najmniejszy obszar ogólnej aplikacji - w tym przypadku klasę sieci.
Ostatecznie nie ma magicznej kuli do testowania, musisz podjąć rozsądne decyzje dotyczące tego, ile i przy jakim stopniu szczegółowości zastosujesz swoje ograniczone zasoby testowe. Podejście oparte na narzędziach, które automatycznie generuje kody pośredniczące, nie robi tego, jest to podejście tępe.
Biorąc to pod uwagę, nie musisz konstruować kodu w określony sposób, aby osiągnąć TDD, ale poziom przeprowadzanych testów będzie zależeć od struktury kodu - jeśli masz monolityczny interfejs GUI, którego cała logika jest ściśle związana z w strukturze GUI, wtedy będzie ci trudniej wyodrębnić te elementy, ale nadal możesz napisać test jednostkowy, w którym „jednostka” odnosi się do GUI, a cała praca z zapleczem DB jest wyśmiewana. Jest to skrajny przykład, ale pokazuje, że nadal możesz wykonywać na nim automatyczne testy.
Skutek uboczny strukturyzacji kodu w celu ułatwienia testowania mniejszych jednostek pomaga lepiej zdefiniować aplikację i pozwala łatwiej wymieniać części. Pomaga to również w kodowaniu, ponieważ mniej prawdopodobne jest, że 2 deweloperów będzie pracować nad tym samym komponentem w danym momencie - w przeciwieństwie do monolitycznej aplikacji, która ma przenikane zależności, które przerywają pracę wszystkich innych, przychodzi czas na scalenie.
źródło
Osiągnąłeś dobre wyniki na temat kompromisów w projektowaniu języka. Niektóre kluczowe decyzje projektowe w C ++ (mechanizm funkcji wirtualnej zmieszany z mechanizmem statycznego wywołania funkcji) sprawiają, że TDD jest trudne. Język tak naprawdę nie obsługuje tego, czego potrzebujesz, aby ułatwić. Łatwo jest napisać C ++, którego testowanie jednostkowe jest prawie niemożliwe.
Mieliśmy więcej szczęścia, robiąc nasz kod TDD C ++ z quasi-funkcjonalnego sposobu myślenia - pisz funkcje, a nie procedury (funkcja, która nie przyjmuje argumentów i zwraca nieważność) i używaj kompozycji tam, gdzie to możliwe. Ponieważ trudno jest zastąpić te klasy składowe, skupiamy się na testowaniu tych klas w celu zbudowania zaufanej bazy, a następnie wiemy, że podstawowe funkcje jednostki, gdy dodamy ją do czegoś innego.
Kluczem jest podejście quasi-funkcjonalne. Pomyśl o tym, jeśli cały twój kod C ++ byłby darmowymi funkcjami, które nie uzyskiwały dostępu do globali, byłoby to łatwe do przeprowadzenia testu jednostkowego :)
źródło