Czy istnieje powód, dla którego testy nie są napisane zgodnie z testowanym kodem?

91

Niedawno czytałem trochę o Programowaniu Literackim i przyszło mi to do głowy ... Dobrze napisane testy, szczególnie specyfikacje w stylu BDD, potrafią lepiej wyjaśnić, co robi kod niż proza, i mają dużą zaletę weryfikacja ich własnej dokładności.

Nigdy nie widziałem testów napisanych zgodnie z testowanym kodem. Czy dzieje się tak tylko dlatego, że języki zwykle nie ułatwiają oddzielania kodu aplikacji i kodu testowego, gdy są zapisywane w tym samym pliku źródłowym (i nikt tego nie ułatwił), czy też istnieje bardziej zasadniczy powód oddzielania kodu testowego od kodu aplikacji?

Chris Devereux
źródło
33
Niektóre języki programowania, takie jak Python z doctest, pozwalają to zrobić.
Simon Bergot,
2
Możesz czuć, że specyfikacje w stylu BDD są lepsze niż proza ​​w wyjaśnianiu kodu, ale to nie znaczy, że kombinacja tych dwóch nie jest lepsza.
JeffO,
5
Połowa argumentów tutaj dotyczy również dokumentacji wbudowanej.
CodesInChaos
3
@Simon doctests są zbyt uproszczone do poważnych testów, głównie dlatego, że nie są do tego przeznaczone. Zostały zaprojektowane i wyróżniają się przykładami kodu w dokumentacji, które można zweryfikować automatycznie. Teraz niektórzy ludzie używają ich również do testowania jednostkowego, ale ostatnio (jak w poprzednich latach) zajęło to dużo błędów, ponieważ zwykle kończy się delikatnymi bałaganami, nadmiernie szczegółową „dokumentacją” i innymi bałaganami.
7
Projektowanie według umowy pozwala na wprowadzanie specyfikacji, które ułatwiają testowanie.
Fuhrmanator,

Odpowiedzi:

89

Jedyną korzyścią, jaką mogę wymyślić w przypadku testów wbudowanych, byłoby zmniejszenie liczby plików do zapisania. W przypadku nowoczesnych IDE to naprawdę nie jest taka wielka sprawa.

Testy wbudowane mają jednak wiele oczywistych wad:

  • Narusza to rozdzielenie obaw . Może to być dyskusyjne, ale dla mnie testowanie funkcjonalności to inna odpowiedzialność niż implementacja.
  • Będziesz musiał albo wprowadzić nowe funkcje językowe, aby odróżnić testy / implementację, albo ryzykujesz zatarciem granicy między nimi.
  • Większe pliki źródłowe są trudniejsze w obsłudze: trudniejsze do odczytania, trudniejsze do zrozumienia, częściej masz do czynienia z konfliktami kontroli źródła.
  • Myślę, że trudniej byłoby założyć „tester” kapelusz, że tak powiem. Jeśli patrzysz na szczegóły implementacji, będziesz bardziej kuszony, aby pominąć wdrożenie niektórych testów.
Vaughandroid
źródło
9
To interesujące. Myślę, że zaletą, którą widzę, jest to, że kiedy masz na sobie kapelusz „programisty”, chcesz myśleć o testach, ale dobrze, że sytuacja odwrotna nie jest prawdą.
Chris Devereux
2
Wzdłuż tych linii możliwe jest (i być może pożądane), aby jedna osoba tworzyła testy, a druga faktycznie implementowała kod. Umieszczenie testów w linii sprawia, że ​​jest to trudniejsze.
Jim Nutt,
6
oddałbym głos, gdybym mógł. Jak w ogóle jest to odpowiedź? Realizatorzy nie piszą testów? Ludzie pomijają testy, jeśli patrzą na szczegóły implementacji? „Po prostu zbyt mocno” Konflikty na dużych plikach? I jak w jakikolwiek sposób można pomylić test ze szczegółami implementacji?
bharal
5
@bharal Także, wrt do „Po prostu zbyt mocno”, masochizm jest cnotą głupców. Chcę, żeby wszystko było łatwe, z wyjątkiem problemu, który próbuję rozwiązać.
deworde
3
Test jednostkowy można uznać za dokumentację. Sugeruje to, że testy jednostkowe powinny być zawarte w kodzie z tego samego powodu, co komentarze - w celu poprawy czytelności. Jednak problem polega na tym, że jest wiele testów jednostkowych i dużo narzutów związanych z implementacją testów, które nie określają oczekiwanych wyników. Nawet komentarze w kodzie powinny być zwięzłe, z większymi objaśnieniami odsuniętymi na bok - do bloku komentarzy poza funkcją, do osobnego pliku, a może do dokumentu projektowego. Testy jednostkowe są IMO rzadko, jeśli w ogóle wystarczająco krótkie, aby utrzymać w testowanym kodzie jak komentarze.
Steve314
36

Mogę wymyślić kilka:

  • Czytelność. Przeplatanie „prawdziwego” kodu i testów utrudni odczytanie prawdziwego kodu.

  • Nadęty kod. Mieszanie „prawdziwego” kodu i kodu testowego w te same pliki / klasy / cokolwiek, co może skutkować większymi skompilowanymi plikami itp. Jest to szczególnie ważne w przypadku języków z późnym wiązaniem.

  • Możesz nie chcieć, aby Twoi klienci / klienci widzieli Twój kod testowy. (Nie podoba mi się ten powód ... ale jeśli pracujesz nad projektem o zamkniętym źródle, kod testowy i tak raczej nie pomoże klientowi).

Teraz możliwe są obejścia każdego z tych problemów. Ale IMO, łatwiej jest nie iść tam.


Warto zauważyć, że na początku programiści Java robili takie rzeczy; np. włączenie main(...)metody do klasy w celu ułatwienia testowania. Ten pomysł prawie całkowicie zniknął. Praktyką branżową jest wdrażanie testów osobno przy użyciu pewnego rodzaju ram testowych.

Warto również zauważyć, że Literate Programming (według Knutha) nigdy nie przyjęło się w branży inżynierii oprogramowania.

Stephen C.
źródło
4
+1 Problemy z czytelnością - kod testowy może być proporcjonalnie większy niż kod implementacyjny, szczególnie w projektach OO.
Fuhrmanator,
2
+1 za wskazanie za pomocą ram testowych. Nie wyobrażam sobie używania dobrego szkieletu testowego jednocześnie z kodem produkcyjnym.
joshin4colours
1
RE: Możesz nie chcieć, aby Twoi klienci / klienci widzieli Twój kod testowy. (Nie podoba mi się ten powód ... ale jeśli pracujesz nad projektem o zamkniętym źródle, kod testowy i tak raczej nie pomoże klientowi.) - Może być pożądane uruchomienie testów na komputerze klienta. Przeprowadzenie testów może pomóc w szybkim zidentyfikowaniu problemu i zidentyfikowaniu różnic w środowisku klientów.
sixtyfootersdude
1
@sixtyfootersdude - to dość niezwykła sytuacja. Zakładając, że tworzysz zamknięte źródło, na wszelki wypadek nie chciałbyś włączać swoich testów do standardowej dystrybucji binarnej. (Utworzyłbyś oddzielny pakiet zawierający testy, które chcesz uruchomić klienta.)
Stephen C
1
1) Czy przegapiłeś pierwszą część mojej odpowiedzi, w której podałem trzy rzeczywiste powody? W grę wchodziła „myśl krytyczna”… 2) Czy przegapiłeś drugą część, w której powiedziałem, że programiści Java to robili, ale teraz nie? A oczywiste implikacje, że programiści przestali to robić ... nie bez powodu?
Stephen C,
14

Właściwie można to traktować jako Design By Contract . Problem polega na tym, że większość języków programowania nie pozwala na pisanie kodu w ten sposób :( Bardzo łatwo jest ręcznie przetestować warunki wstępne, ale warunki pocztowe są prawdziwym wyzwaniem bez zmiany sposobu pisania kodu (ogromny negatywny IMO).

Michael Feathers ma prezentację na ten temat i jest to jeden z wielu sposobów, o których wspomina, że ​​można poprawić jakość kodu.

Daniel Kaplan
źródło
13

Z wielu tych samych powodów, dla których starasz się unikać ścisłego łączenia klas w kodzie, dobrym pomysłem jest również unikanie niepotrzebnego łączenia między testami i kodem.

Tworzenie: Testy i kod mogą być pisane w różnym czasie przez różne osoby.

Kontrola: jeśli testy są używane do określenia wymagań, na pewno chcesz, aby podlegały one różnym zasadom określającym, kto może je zmienić i kiedy nie jest to rzeczywisty kod.

Wielokrotnego użytku : Jeśli umieścisz testy bezpośrednio, nie możesz ich użyć z innym fragmentem kodu.

Wyobraź sobie, że masz fragment kodu, który poprawnie wykonuje zadanie, ale pozostawia wiele do życzenia pod względem wydajności, łatwości konserwacji, cokolwiek. Zdecydujesz się zastąpić ten kod nowym i ulepszonym kodem. Korzystanie z tego samego zestawu testów może pomóc zweryfikować, czy nowy kod daje takie same wyniki jak stary kod.

Możliwość wyboru: Oddzielenie testów od kodu ułatwia wybór testów, które chcesz uruchomić.

Na przykład możesz mieć mały pakiet testów, które dotyczą tylko kodu, nad którym obecnie pracujesz, oraz większy pakiet, który testuje cały projekt.

Caleb
źródło
Zastanawiam się nad twoimi powodami: TDD mówi już, że tworzenie testów odbywa się przed (lub w tym samym czasie) kodem produkcyjnym i musi być wykonane przez tego samego programistę! Wskazują również, że testy są bardzo podobne do wymagań. Oczywiście te zastrzeżenia nie mają zastosowania, jeśli nie wyznajesz dogmatów TDD (co byłoby dopuszczalne, ale musisz to wyjaśnić!). Czym dokładnie jest test „wielokrotnego użytku”? Czy testy z definicji nie są specyficzne dla testowanego kodu?
Andres F.,
1
@AndresF. Nie, testy nie są specyficzne dla testowanego kodu; są specyficzne dla zachowania, które testują. Załóżmy, że masz moduł Widget z zestawem testów, które sprawdzają, czy Widget działa poprawnie. Twój kolega wymyślił BetterWidget, który rzekomo robi to samo co Widget, ale trzy razy szybciej. Jeśli testy Widżetu są osadzone w kodzie źródłowym Widżetu w taki sam sposób, jak Literate Programming osadza dokumentację w kodzie źródłowym, nie można bardzo dobrze zastosować tych testów do BetterWidget, aby sprawdzić, czy zachowuje się tak samo, jak Widget.
Caleb
@AndresF. nie trzeba określać, że nie przestrzegasz TDD. to nie jest kosmiczna wartość domyślna. Co do punktu ponownego wykorzystania. Podczas testowania systemu zależy Ci na wejściach i wyjściach, a nie na elementach wewnętrznych. Kiedy musisz stworzyć nowy system, który zachowuje się tak samo, ale jest zaimplementowany inaczej, dobrze jest mieć testy, które można uruchomić zarówno na starym, jak i na nowym systemie. zdarzyło mi się to więcej niż raz, czasem trzeba pracować nad nowym systemem, podczas gdy stary jest wciąż w fazie produkcyjnej lub nawet uruchomić je obok siebie. spójrz na sposób, w jaki Facebook testował „reagowanie na światłowód” testami reagowania, aby uzyskać parzystość.
user1852503
10

Oto kilka dodatkowych powodów, dla których mogę wymyślić:

  • posiadanie testów w oddzielnej bibliotece ułatwia łączenie tylko tej biblioteki ze środowiskiem testowym, a nie z kodem produkcyjnym (tego można uniknąć w przypadku niektórych preprocesorów, ale po co budować takie rzeczy, gdy łatwiejszym rozwiązaniem jest napisanie testów w osobne miejsce)

  • testy funkcji, klasy, biblioteki są zwykle pisane z punktu widzenia „użytkowników” (użytkownik tej funkcji / klasy / biblioteki). Taki „kod” jest zwykle zapisywany w osobnym pliku lub bibliotece, a test może być jaśniejszy lub „bardziej realistyczny”, jeśli naśladuje tę sytuację.

Doktor Brown
źródło
5

Jeśli testy byłyby wbudowane, konieczne byłoby usunięcie kodu potrzebnego do testowania podczas wysyłania produktu do klienta. Tak więc dodatkowe miejsce, w którym przechowujesz swoje testy, po prostu dzieli kod, którego potrzebujesz i kodu, którego potrzebuje klient .

mhr
źródło
9
Niemożliwe. Wymagałoby to dodatkowej fazy wstępnego przetwarzania, podobnie jak LP. Można to łatwo zrobić na przykład w języku C lub w języku kompilacji do js.
Chris Devereux,
+1 za wskazanie mi tego. Zredagowałem moją odpowiedź, aby to przedstawić.
mrs
Istnieje również założenie, że rozmiar kodu ma znaczenie w każdym przypadku. To, że ma znaczenie w niektórych przypadkach, nie oznacza, że ​​ma to znaczenie we wszystkich przypadkach. Istnieje wiele środowisk, w których programiści nie są zmuszeni do optymalizacji rozmiaru kodu źródłowego. Gdyby tak było, nie stworzyliby tak wielu klas.
zumalifeguard,
5

Pomysł ten stanowi po prostu metodę „Self_Test” w kontekście projektowania obiektowego lub obiektowego. Jeśli używasz skompilowanego języka obiektowego, takiego jak Ada, cały kod autotestu zostanie oznaczony przez kompilator jako nieużywany (nigdy nie wywołany) podczas kompilacji produkcyjnej, a zatem wszystko zostanie zoptymalizowane - żaden z nich nie pojawi się w wynikowy plik wykonywalny.

Korzystanie z metody „Self_Test” jest niezwykle dobrym pomysłem, a gdyby programiści naprawdę martwili się jakością, wszyscy by to robili. Jedną ważną kwestią jest to, że metoda „Self_Test” wymaga intensywnej dyscypliny, ponieważ nie może uzyskać dostępu do żadnych szczegółów implementacji i zamiast tego musi polegać tylko na wszystkich innych opublikowanych metodach w specyfikacji obiektu. Oczywiście, jeśli autotest nie powiedzie się, implementacja będzie musiała ulec zmianie. Autotest powinien rygorystycznie testować wszystkie opublikowane właściwości metod obiektu, ale nigdy nie polegać w żaden sposób na szczegółach żadnej konkretnej implementacji.

Języki obiektowe i zorientowane obiektowo często zapewniają dokładnie ten rodzaj dyscypliny w odniesieniu do metod zewnętrznych w stosunku do testowanego obiektu (wymuszają specyfikację obiektu, uniemożliwiając dostęp do szczegółów jego implementacji i zgłaszając błąd kompilacji w przypadku wykrycia takiej próby ). Ale wszystkie wewnętrzne metody obiektu mają pełny dostęp do każdego szczegółu implementacji. Tak więc metoda autotestu znajduje się w wyjątkowej sytuacji: ze względu na swój charakter musi być metodą wewnętrzną (autotest jest oczywiście metodą testowanego obiektu), ale musi zostać uwzględniona cała dyscyplina kompilatora metody zewnętrznej ( musi być niezależny od szczegółów implementacji obiektu). Niewiele, jeśli jakiekolwiek języki programowania umożliwiają dyscyplinowanie obiektu ” metoda wewnętrzna, jakby była metodą zewnętrzną. Jest to więc ważny problem projektowania języka programowania.

W przypadku braku odpowiedniej obsługi języka programowania najlepszym sposobem na to jest utworzenie obiektu towarzyszącego. Innymi słowy, dla każdego kodowanego obiektu (nazwijmy go „Big_Object”) tworzysz również drugi obiekt towarzyszący, którego nazwa składa się ze standardowego sufiksu połączonego z nazwą „prawdziwego” obiektu (w tym przypadku „Big_Object_Self_Test ”), a specyfikacja składa się z jednej metody („ Big_Object_Self_Test.Self_Test (This_Big_Object: Big_Object) zwraca wartość logiczną; ”). Obiekt towarzyszący będzie wówczas zależał od specyfikacji głównego obiektu, a kompilator w pełni wymusi całą dyscyplinę tej specyfikacji względem implementacji obiektu towarzyszącego.

komentator8
źródło
4

Jest to odpowiedź na dużą liczbę komentarzy sugerujących, że testy wbudowane nie są wykonywane, ponieważ usunięcie kodu testowego z kompilacji wydania jest trudne. To nieprawda. Prawie wszystkie kompilatory i asemblery już to obsługują, w przypadku skompilowanych języków, takich jak C, C ++, C #, odbywa się to za pomocą tak zwanych dyrektyw kompilatora.

W przypadku c # (również uważam, że c ++, składnia może się nieco różnić w zależności od używanego kompilatora), w ten sposób możesz to zrobić.

#define DEBUG //  = true if c++ code
#define TEST /* can also be defined in the make file for c++ or project file for c# and applies to all associated .cs/.cpp files */

//somewhere in your code
#if DEBUG
// debug only code
#elif TEST
// test only code
#endif

Ponieważ używa to dyrektyw kompilatora, kod nie będzie istniał w plikach wykonywalnych, które są budowane, jeśli flagi nie są ustawione. W ten sposób tworzysz programy „pisz raz, kompiluj dwa razy” dla wielu platform / sprzętu.

Jan
źródło
2

Używamy testów wbudowanych z naszym kodem Perl. Istnieje moduł Test :: Inline , który generuje pliki testowe z kodu wbudowanego.

Nie jestem szczególnie dobry w organizowaniu moich testów i stwierdziłem, że są one łatwiejsze i istnieje większe prawdopodobieństwo, że zostaną zachowane, gdy zostaną wprowadzone.

W odpowiedzi na kilka podniesionych obaw:

  • Testy wbudowane są napisane w sekcjach POD, więc nie są częścią rzeczywistego kodu. Są one ignorowane przez interpretera, więc nie ma rozdęcia kodu.
  • Używamy składania Vima, aby ukryć sekcje testowe. Jedyne, co widzisz, to jedna linia powyżej każdej testowanej metody +-- 33 lines: #test----. Kiedy chcesz pracować z testem, po prostu go rozwiń.
  • Moduł Test :: Inline „kompiluje” testy do normalnych plików zgodnych z TAP, dzięki czemu mogą one współistnieć z tradycyjnymi testami.

Na przykład:

mla
źródło
1

Erlang 2 faktycznie obsługuje testy wbudowane. Każde wyrażenie boolowskie w kodzie, które nie jest używane (np. Przypisane do zmiennej lub przekazane) jest automatycznie traktowane jako test i oceniane przez kompilator; jeśli wyrażenie jest fałszywe, kod się nie kompiluje.

Mark Rendle
źródło
1

Innym powodem oddzielania testów jest to, że często używasz dodatkowych lub nawet innych bibliotek do testowania niż do rzeczywistej implementacji. Jeśli pomieszane zostaną testy i implementacja, kompilator nie może przechwycić przypadkowego użycia bibliotek testowych w implementacji.

Ponadto testy zwykle zawierają znacznie więcej wierszy kodu niż testowane części implementacji, więc będziesz mieć problemy ze znalezieniem implementacji między wszystkimi testami. :-)

Hans-Peter Störr
źródło
0

To nie jest prawda. Znacznie lepiej jest umieścić testy jednostkowe obok kodu produkcyjnego, gdy kod produkcyjny, szczególnie gdy procedura produkcyjna jest czysta.

Jeśli tworzysz na przykład w .NET, możesz umieścić kod testowy w zestawie produkcyjnym, a następnie użyć Skalpela, aby usunąć go przed wysyłką.

zumalifeguard
źródło