Czy testy jednostkowe prowadzą do przedwczesnego uogólnienia (szczególnie w kontekście C ++)?


20

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?


8
Powszechnym memem jest to, że każdy wzór można nadużyć, aby stać się anty-wzorem. To samo dotyczy TDD. Można dodać testowalne interfejsy powyżej punktu malejących zwrotów, gdzie testowany kod jest mniejszy niż dodany uogólniony interfejs testowy, a także w obszarze zbyt niskich kosztów i korzyści. Zwykła gra z dodatkowymi interfejsami do testowania, jak system operacyjny kosmicznej misji, może całkowicie przegapić okno rynkowe. Upewnij się, że dodane badanie znajduje się przed punktami przegięcia.
hotpaw2

@ hotpaw2 Blasphemy! :)
wałek klonowy

Odpowiedzi:


23

Test jednostkowy - szczególnie w C ++ - wymaga od testowanego kodu dodania kolejnych szwów, które byłyby ściśle wymagane dla danego problemu.

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.

Czy sposób, w jaki testy jednostkowe wymagają, aby struktura kodu aplikacji była „tylko” korzystna dla testów jednostkowych, czy faktycznie jest korzystny dla struktury aplikacji?

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:

  • Testowanie projektu zmusza cię do podzielenia aplikacji na małe, mniej lub bardziej niezależne części, które mogą wpływać na siebie tylko w ograniczony i dobrze określony sposób - jest to bardzo ważne dla długoterminowej stabilności i możliwości utrzymania twojego programu. Bez tego kod zmienia się w kod spaghetti, w którym każda zmiana dokonana w dowolnej części bazy kodu może spowodować nieoczekiwane efekty w pozornie niezwiązanych, odrębnych częściach programu. Co, oczywiście, koszmar każdego programisty.
  • Samo pisanie testów w stylu TDD ćwiczy interfejsy API, klasy i metody oraz służy jako bardzo skuteczny test do wykrycia, czy Twój projekt ma sens - jeśli pisanie testów przeciwko i interfejsowi wydaje się niewygodne lub trudne, otrzymujesz cenne wczesne informacje zwrotne, gdy są nadal łatwo kształtować API. Innymi słowy, chroni to przed przedwczesnym opublikowaniem interfejsów API.
  • Wzorzec rozwoju wymuszony przez TDD pomaga skupić się na konkretnych zadaniach do wykonania i utrzymuje cel, minimalizując szanse na odejście od rozwiązania innych problemów niż ten, do którego należy, dodając niepotrzebne dodatkowe funkcje i złożoność itd.
  • Szybka informacja zwrotna z testów jednostkowych pozwala odważnie przefaktoryzować kod, umożliwiając ciągłe dostosowywanie i ewolucję projektu przez cały okres istnienia kodu, skutecznie zapobiegając entropii kodu.

Pamiętam ogólną zasadę, która mówi: nie generalizuj, dopóki nie musisz / 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?

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.


Pozdrawiam pana.
GordonM,

Krótka, ale pedantyczna uwaga: „port diagnostyczny” istnieje głównie dlatego, że rządy upoważniły ich w ramach programu kontroli emisji. W związku z tym ma poważne ograniczenia; istnieje wiele rzeczy, które potencjalnie można zdiagnozować za pomocą tego portu, a które nie są (tj. nie mają nic wspólnego z kontrolą emisji).
Robert Harvey

4

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 .


Chciałem dodać to samo: zrób API, przetestuj API z zewnątrz.
Christopher Mahan

2

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.


1

testowanie najmniejszej możliwej do izolacji jednostki aplikacji

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.


0

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 :)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.