W jaki sposób testy jednostkowe ułatwiają projektowanie?


43

Nasz kolega promuje pisanie testów jednostkowych jako faktycznie pomagających nam udoskonalić nasz projekt i refaktoryzować rzeczy, ale nie rozumiem, jak to zrobić. Jeśli ładuję plik CSV i analizuję go, w jaki sposób test jednostkowy (sprawdzanie poprawności wartości w polach) pomoże mi zweryfikować mój projekt? Wspomniał o sprzężeniu i modułowości itp., Ale dla mnie nie ma to większego sensu - ale nie mam zbyt dużego zaplecza teoretycznego.

To nie to samo, co pytanie, które zaznaczyłeś jako duplikat. Byłbym zainteresowany rzeczywistymi przykładami, w jaki sposób to pomaga, a nie tylko teorią mówiącą „pomaga”. Podoba mi się odpowiedź poniżej i komentarz, ale chciałbym dowiedzieć się więcej.



3
Odpowiedź poniżej to naprawdę wszystko, co musisz wiedzieć. Siedzi obok ludzi, którzy piszą przez cały dzień fabryki wstrzykiwane przez agregację fabryk fabrycznych zależności od korzeni, jest facetem, który cicho pisze prosty kod do testów jednostkowych, który działa poprawnie, jest łatwy do zweryfikowania i jest już udokumentowany.
Robert Harvey

4
@gnat przeprowadzanie testów jednostkowych nie implikuje automatycznie TDD, to inne pytanie
Joppe

11
„test jednostkowy (sprawdzanie poprawności wartości w polach)” - wydaje się, że łączysz testy jednostkowe z weryfikacją danych wejściowych.
jonrsharpe

1
@jonrsharpe Biorąc pod uwagę, że kod parsuje plik CSV, może on mówić o prawdziwym teście jednostkowym, który sprawdza, czy określony ciąg CSV daje oczekiwany wynik.
JollyJoker,

Odpowiedzi:


3

Testy jednostkowe nie tylko ułatwiają projektowanie, ale jest to jedna z ich głównych zalet.

Napisanie pierwszego testu eliminuje modułowość i czystą strukturę kodu.

Kiedy piszesz swój kod w pierwszej kolejności, przekonasz się, że wszelkie „warunki” danej jednostki kodu są naturalnie wypychane do zależności (zwykle poprzez makiety lub kody pośredniczące), kiedy przyjmujesz je w swoim kodzie.

„Biorąc pod uwagę warunek x, spodziewaj się zachowania y”, często staje się odgałęzieniem do dostarczenia x(co jest scenariuszem, w którym test musi zweryfikować zachowanie bieżącego komponentu) i ystaje się próbą, której wywołanie zostanie zweryfikowane na koniec testu (chyba że jest to „powinien zwrócić y”), w którym to przypadku test po prostu wyraźnie zweryfikuje zwracaną wartość).

Następnie, gdy jednostka zachowuje się tak, jak określono, przechodzisz do pisania odkrytych zależności (dla xi y).

To sprawia, że ​​pisanie czystego, modułowego kodu jest bardzo łatwym i naturalnym procesem, w przeciwnym razie często łatwo jest rozmazać obowiązki i powiązać zachowania bez uświadomienia sobie tego.

Pisząc testy później dowiesz się, kiedy twój kod ma słabą strukturę.

Kiedy pisanie testów dla fragmentu kodu staje się trudne, ponieważ jest zbyt wiele rzeczy do skartowania lub wyszydzenia, lub ponieważ rzeczy są zbyt ściśle ze sobą powiązane, wiesz, że masz ulepszenia w kodzie.

Kiedy „zmiana testów” staje się ciężarem, ponieważ w jednej jednostce jest tak wiele zachowań, wiesz, że masz ulepszenia w kodzie (lub po prostu w podejściu do pisania testów - ale z mojego doświadczenia nie jest tak zwykle) .

Gdy staje się zbyt skomplikowane scenariusze ( „jeśli xa y, a zpotem ...”), ponieważ trzeba bardziej abstrakcyjnego, wiesz, że masz ulepszenia wprowadzone w kodzie.

Kiedy skończysz z tymi samymi testami w dwóch różnych urządzeniach ze względu na powielanie i redundancję, wiesz, że masz ulepszenia w kodzie.

Oto doskonała rozmowa Michaela Feathersa, pokazująca bardzo ścisły związek między testowalnością i projektowaniem w kodzie (pierwotnie opublikowana przez displayName w komentarzach). Rozmowa dotyczy również niektórych powszechnych skarg i nieporozumień na temat dobrego projektu i ogólnie testowalności.


@SSECommunity: Przy zaledwie 2 pozytywnych opiniach na dziś odpowiedź ta jest bardzo łatwa do przeoczenia. Gorąco polecam Dyskusję Michaela Feathersa, która została powiązana z tą odpowiedzią.
displayName

103

Wspaniałą rzeczą w testach jednostkowych jest to, że pozwalają ci używać twojego kodu, tak jak inni programiści będą go używać.

Jeśli twój kod jest trudny do testowania jednostkowego, prawdopodobnie będzie trudny w użyciu. Jeśli nie możesz wstrzykiwać zależności bez przeskakiwania przez obręcze, prawdopodobnie kod będzie mało elastyczny w użyciu. A jeśli musisz poświęcić dużo czasu na konfigurowanie danych lub zastanawianie się nad kolejnością wykonywania zadań, testowany kod prawdopodobnie ma zbyt wiele sprzężeń i będzie trudny w pracy.


7
Świetna odpowiedź. Zawsze lubię myśleć o moich testach jako o pierwszym kliencie kodu; jeśli pisanie testów jest bolesne, pisanie kodu, który pochłania API lub cokolwiek, co rozwijam, będzie bolesne.
Stephen Byrne

41
Z mojego doświadczenia wynika, że ​​większość testów jednostkowych „ nie używa twojego kodu, tak jak inni programiści będą go używać”. Używają Twojego kodu, ponieważ testy jednostkowe będą go używać. To prawda, że ​​ujawnią wiele poważnych wad. Ale interfejs API zaprojektowany do testowania jednostkowego może nie być interfejsem API najbardziej odpowiednim do ogólnego użytku. Uproszczone testy jednostkowe często wymagają, aby kod źródłowy ujawnił zbyt wiele elementów wewnętrznych. Ponownie, w oparciu o moje doświadczenie - chciałbym usłyszeć, jak sobie z tym poradziłeś. (Zobacz moją odpowiedź poniżej)
user949300

7
@ user949300 - Najpierw nie jestem zwolennikiem testowania. Moja odpowiedź opiera się najpierw na idei kodu (a na pewno projektu). Interfejsy API nie powinny być przeznaczone do testowania jednostkowego, powinny być zaprojektowane dla Twojego klienta. Testy jednostkowe pomagają przybliżyć klienta, ale są narzędziem. Są po to, aby ci służyć, a nie odwrotnie. I z pewnością nie powstrzymają cię przed tworzeniem gównianego kodu.
Telastyn

3
Największy problem z testami jednostkowymi z mojego doświadczenia polega na tym, że pisanie dobrych jest tak samo trudne jak pisanie dobrego kodu. Jeśli nie potrafisz odróżnić dobrego kodu od złego, pisanie testów jednostkowych nie poprawi kodu. Pisząc test jednostkowy, musisz umieć odróżnić płynne, przyjemne użytkowanie od „niezręcznego” lub trudnego. Mogą sprawić, że trochę użyjesz kodu, ale nie zmuszają cię do uznania, że ​​to, co robisz, jest złe.
jpmc26,

2
@ user949300 - klasyczny przykład, który miałem na myśli, to Repozytorium, które wymaga connString. Załóżmy, że ujawniasz to jako publiczną właściwość do zapisu i musisz ustawić ją po utworzeniu repozytorium (). Chodzi o to, że po piątym lub szóstym razem napisałeś test, który zapomina zrobić ten krok - i tym samym ulega awarii - będziesz "naturalnie" skłonny do zmuszania connString do pozostania niezmiennikiem klasy - przekazany w konstruktorze - czyniąc Lepszy interfejs API i zwiększenie prawdopodobieństwa napisania kodu produkcyjnego, który pozwala uniknąć tej pułapki. To nie jest gwarancja, ale pomaga, imo.
Stephen Byrne,

31

Zajęło mi to sporo czasu, ale prawdziwą korzyścią (edytuj: dla mnie twój przebieg może się różnić) z robienia testów opartych na testach ( przy użyciu testów jednostkowych) jest to, że musisz zaprojektować API z góry !

Typowe podejście do programowania polega na tym, aby najpierw dowiedzieć się, jak rozwiązać dany problem, a dzięki tej wiedzy i projektowi wstępnego wdrożenia można wywołać rozwiązanie. Może to dać dość interesujące wyniki.

Robiąc TDD, musisz jako pierwszy napisać kod, który użyje twojego rozwiązania. Parametry wejściowe i oczekiwane dane wyjściowe, aby upewnić się, że są prawidłowe. To z kolei wymaga, abyś zorientował się, co tak naprawdę musisz zrobić, abyś mógł tworzyć sensowne testy. Wtedy i tylko wtedy wdrażasz rozwiązanie. Z mojego doświadczenia wynika również, że kiedy dokładnie wiesz, co ma osiągnąć Twój kod, staje się on wyraźniejszy.

Następnie po wdrożeniu testy jednostkowe pomagają upewnić się, że refaktoryzacja nie psuje funkcjonalności, oraz dostarczają dokumentację na temat korzystania z kodu (który, jak wiesz, jest poprawny po zdaniu testu!). Ale są one drugorzędne - największą korzyścią jest sposób myślenia przy tworzeniu kodu.


Jest to z pewnością korzyść, ale nie sądzę, że jest to „prawdziwa” korzyść - rzeczywista korzyść wynika z faktu, że pisanie testów dla twojego kodu w naturalny sposób wypycha „warunki” do zależności i eliminuje nadmierne wstrzykiwanie zależności (dalsze promowanie abstrakcji ) przed rozpoczęciem.
Ant P

Problem polega na tym, że piszesz cały zestaw testów, które pasują do tego API, wtedy nie działa dokładnie tak, jak trzeba i musisz przepisać kod i wszystkie testy. W przypadku publicznych interfejsów API prawdopodobnie się nie zmienią i takie podejście jest w porządku. Jednak interfejsy API dla kodu, który jest używany tylko wewnętrznie, bardzo się zmieniają, gdy wymyślisz, jak zaimplementować funkcję, która wymaga dużej ilości półprywatnych API współpracujących ze sobą
Juan Mendes

@AntP Tak, jest to część projektu interfejsu API.
Thorbjørn Ravn Andersen

@JuanMendes Nie jest to rzadkie i te testy będą musiały zostać zmienione, tak jak każdy inny kod podczas zmiany wymagań. Dobre IDE pomoże ci refaktoryzować klasy w ramach pracy wykonywanej automatycznie po zmianie sygnatur metody itp.
Thorbjørn Ravn Andersen

@JuanMendes, jeśli piszesz dobre testy i małe jednostki, wpływ opisywanego efektu jest w praktyce niewielki do żadnego.
Ant P

6

Zgadzam się w 100%, że testy jednostkowe pomagają „pomóc nam udoskonalić nasz projekt i przefakturować rzeczy”.

Nie mam pojęcia, czy pomogą ci wykonać wstępny projekt . Tak, ujawniają oczywiste wady i zmuszają cię do zastanowienia się nad tym, „w jaki sposób mogę przetestować kod”? Powinno to prowadzić do mniejszej liczby skutków ubocznych, łatwiejszej konfiguracji i konfiguracji itp.

Jednak z mojego doświadczenia wynika, że ​​zbyt uproszczone testy jednostkowe, napisane zanim naprawdę zrozumiesz, jaki powinien być projekt (co prawda, to przesada w przypadku twardego TDD, ale zbyt często koderzy piszą test, zanim dużo pomyślą) często prowadzą do anemii modele domen, które ujawniają zbyt wiele elementów wewnętrznych.

Moje doświadczenie z TDD było kilka lat temu, więc jestem zainteresowany usłyszeniem, jakie nowsze techniki mogą pomóc w pisaniu testów, które nie wpływają zbytnio na projekt podstawowy. Dzięki.


Duża liczba parametrów metody to zapach kodu i wada projektowa.
Sufian

5

Test jednostkowy pozwala zobaczyć, jak działają interfejsy między funkcjami, i często daje wgląd w to, jak ulepszyć zarówno projekt lokalny, jak i projekt ogólny. Ponadto, jeśli rozwijasz testy jednostkowe podczas opracowywania kodu, masz gotowy pakiet testów regresji. Nie ma znaczenia, czy tworzysz interfejs użytkownika czy bibliotekę zaplecza.

Po opracowaniu programu (z testami jednostkowymi), gdy błędy zostaną wykryte, możesz dodać testy, aby potwierdzić, że błędy zostały usunięte.

Używam TDD do niektórych moich projektów. Włożyłem wiele wysiłku w tworzenie przykładów, które czerpię z podręczników lub artykułów uważanych za poprawne, i testuję kod, który rozwijam, korzystając z tych przykładów. Wszelkie nieporozumienia dotyczące metod stają się bardzo widoczne.

Zwykle jestem nieco luźniejszy niż niektórzy z moich kolegów, ponieważ nie dbam o to, czy kod jest napisany jako pierwszy, czy test jest napisany jako pierwszy.


To dla mnie świetna odpowiedź. Czy mógłbyś podać kilka przykładów, np. Po jednym dla każdego przypadku (gdy uzyskasz wgląd w projektowanie itp.).
User039402

5

Jeśli chcesz przetestować jednostkowo parser poprawnie wykrywający wartości rozdzielające, możesz chcieć przekazać jedną linię z pliku CSV. Aby twój test był bezpośredni i krótki, możesz przetestować go jedną metodą, która akceptuje jedną linię.

Spowoduje to automatyczne oddzielenie odczytu linii od odczytu poszczególnych wartości.

Na innym poziomie możesz nie chcieć umieszczać różnego rodzaju fizycznych plików CSV w swoim projekcie testowym, ale zrobić coś bardziej czytelnego, po prostu deklarując duży ciąg CSV w teście, aby poprawić czytelność i cel testu. Doprowadzi cię to do oddzielenia twojego parsera od dowolnego wejścia / wyjścia, które zrobiłbyś gdzie indziej.

Tylko prosty przykład, po prostu zacznij go ćwiczyć, w pewnym momencie poczujesz magię (mam).


4

Mówiąc prościej, pisanie testów jednostkowych pomaga ujawnić wady w kodzie.

Ten spektakularny przewodnik po pisaniu testowalnego kodu , napisany przez Jonathana Woltera, Russa Ruffera i Miško Hevery'ego, zawiera liczne przykłady tego, jak wady kodu, które blokują testowanie, również uniemożliwiają łatwe ponowne użycie i elastyczność tego samego kodu. Dlatego jeśli kod można przetestować, jest łatwiejszy w użyciu. Większość „moralności” to absurdalnie proste wskazówki, które znacznie poprawiają projektowanie kodu ( Dependency Injection FTW).

Na przykład: bardzo trudno jest przetestować, czy metoda computeStuff działa poprawnie, gdy pamięć podręczna rozpoczyna eksmisję. Jest tak, ponieważ musisz ręcznie dodać crap do pamięci podręcznej, aż „bigCache” będzie prawie pełny.

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Jednak gdy używamy wstrzykiwania zależności, o wiele łatwiej jest sprawdzić, czy metoda computeStuff działa poprawnie, gdy pamięć podręczna zaczyna eksmitować rzeczy. Wszystko, co robimy, to stworzyć test, w którym nazywamy new HereIUseDI(buildSmallCache()); Powiadomienie, mamy bardziej szczegółową kontrolę nad obiektem i natychmiast wypłaca dywidendy.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Podobne korzyści można uzyskać, gdy nasz kod wymaga danych, które zwykle są przechowywane w bazie danych ... wystarczy przekazać DOKŁADNIE potrzebne dane.


2
Szczerze mówiąc, nie jestem pewien, co masz na myśli. W jaki sposób metoda computeStuff odnosi się do pamięci podręcznej?
Jan V

1
@ user970696 - Tak, sugeruję, że „computeStuff ()” używa pamięci podręcznej. Pytanie brzmi: „Czy computeStuff () działa cały czas poprawnie (co zależy od stanu pamięci podręcznej)” W związku z tym trudno jest potwierdzić, że computeStuff () robi to, co chcesz DLA WSZYSTKICH MOŻLIWYCH STANÓW DACHU, jeśli nie możesz bezpośrednio ustawić / zbuduj pamięć podręczną, ponieważ zapisałeś wiersz „cacheOfExpensiveComputation = buildBigCache ();” (w przeciwieństwie do przekazywania pamięci podręcznej bezpośrednio przez konstruktora)
Ivan

0

W zależności od tego, co rozumie się przez „testy jednostkowe”, nie sądzę, aby naprawdę testy jednostkowe na niskim poziomie ułatwiały dobry projekt, podobnie jak testy integracji na nieco wyższym poziomie - testy, które sprawdzają, czy grupa aktorów (klasy, funkcje, cokolwiek) w Twój kod łączy się prawidłowo, aby stworzyć całą masę pożądanych zachowań, które zostały uzgodnione między zespołem programistycznym a właścicielem produktu.

Jeśli umiesz pisać testy na tych poziomach, popycha cię to do stworzenia ładnego, logicznego kodu podobnego do API, który nie wymaga wielu zwariowanych zależności - chęć posiadania prostej konfiguracji testu naturalnie doprowadzi cię do braku dużej ilości szalone zależności lub ściśle powiązany kod.

Nie popełnij jednak błędu - testy jednostkowe mogą prowadzić do złego projektu, a także dobrego projektu. Widziałem, jak programiści biorą trochę kodu, który ma już ładny logiczny projekt i jedną kwestię, i rozbijają go na części i wprowadzają więcej interfejsów wyłącznie w celu testowania, w wyniku czego kod jest mniej czytelny i trudniejszy do zmiany , a może nawet więcej błędów, jeśli deweloper zdecydował, że wiele testów jednostkowych na niskim poziomie oznacza, że ​​nie muszą mieć testów wyższego poziomu. Szczególnym ulubionym przykładem jest naprawiony przeze mnie błąd, w którym było wiele bardzo zepsutego, „testowalnego” kodu związanego z uzyskiwaniem informacji ze schowka i poza nim. Wszystko podzielone i oddzielone do bardzo małych poziomów szczegółowości, z wieloma interfejsami, mnóstwem próbnych testów i innymi zabawnymi rzeczami. Tylko jeden problem - nie było żadnego kodu, który faktycznie wchodziłby w interakcję z mechanizmem schowka systemu operacyjnego,

Testy jednostkowe mogą zdecydowanie wpłynąć na twój projekt - ale nie prowadzą automatycznie do dobrego projektu. Musisz mieć pomysły na to, co to dobry projekt, który wykracza poza „ten kod jest testowany, dlatego jest testowalny, a więc dobry”.

Oczywiście, jeśli jesteś jedną z tych osób, dla których „testy jednostkowe” oznaczają „wszelkie testy automatyczne, które nie są przeprowadzane przez interfejs użytkownika”, niektóre z tych ostrzeżeń mogą nie być tak istotne - jak powiedziałem, myślę, że te wyższe -poziomowe testy integracyjne są często bardziej przydatne, jeśli chodzi o kierowanie projektem.


-2

Testy jednostkowe mogą pomóc w refaktoryzacji, gdy nowy kod przejdzie wszystkie stare testy.

Załóżmy, że wdrożyłeś opcję bąbelków, ponieważ spieszyło ci się i nie martwiłeś wydajnością, ale teraz potrzebujesz szybkiego sortowania, ponieważ dane stają się coraz dłuższe. Jeśli wszystkie testy przejdą pomyślnie, wszystko wygląda dobrze.

Oczywiście testy muszą być kompleksowe, aby to zadziałało. W moim przykładzie twoje testy mogą nie obejmować stabilności, ponieważ nie było to przedmiotem zainteresowania z bąbelkami.


1
Jest to prawda, ale jest to bardziej korzyść w zakresie konserwacji niż bezpośredni wpływ na jakość projektu kodu.
Ant P

@AntP, OP zapytał o refaktoryzację i testy jednostkowe.
om

1
Pytanie wymienionych refactoring ale rzeczywiste pytanie było o tym, jak testy jednostkowe mogą poprawić / zweryfikować projekt kodu - nie ułatwić proces refaktoryzacji się.
Ant P

-3

Odkryłem, że testy jednostkowe są najbardziej cenne dla ułatwienia długoterminowego utrzymania projektu. Kiedy po miesiącach wracam do projektu i nie pamiętam zbyt wielu szczegółów, przeprowadzanie testów powstrzymuje mnie od zepsucia rzeczy.


6
Jest to z pewnością ważny aspekt testów, ale tak naprawdę nie odpowiada na pytanie (nie dlatego testy są dobre, ale jak wpływają na projekt).
Hulk,
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.