Czy powinniśmy projektować nasz kod od samego początku, aby umożliwić testowanie jednostkowe?

91

W naszym zespole toczy się obecnie debata na temat tego, czy modyfikacja projektu kodu w celu umożliwienia testowania jednostkowego jest zapachem kodu lub w jakim stopniu można to zrobić bez zapachu kodu. Stało się tak, ponieważ dopiero zaczynamy wprowadzać praktyki, które są obecne w prawie każdej innej firmie programistycznej.

W szczególności będziemy mieć usługę interfejsu API sieci Web, która będzie bardzo cienka. Jego głównym zadaniem będzie zbieranie żądań / odpowiedzi internetowych i wywoływanie bazowego interfejsu API zawierającego logikę biznesową.

Jednym z przykładów jest to, że planujemy stworzyć fabrykę, która zwróci typ metody uwierzytelnienia. Nie musimy dziedziczyć interfejsu, ponieważ nie spodziewamy się, że będzie to coś innego niż konkretny typ. Aby jednak przetestować usługę Web API, będziemy musieli kpić z tej fabryki.

Zasadniczo oznacza to, że albo projektujemy klasę kontrolera Web API, aby akceptować DI (przez jego konstruktora lub settera), co oznacza, że ​​projektujemy część kontrolera tylko po to, aby umożliwić DI i implementację interfejsu, którego w innym przypadku nie potrzebujemy, lub używamy frameworkiem zewnętrznym, takim jak Ninject, aby uniknąć konieczności zaprojektowania kontrolera w ten sposób, ale nadal będziemy musieli stworzyć interfejs.

Niektórzy członkowie zespołu wydają się niechętnie projektować kod tylko dla celów testowych. Wydaje mi się, że musi być jakiś kompromis, jeśli masz nadzieję na test jednostkowy, ale nie jestem pewien, jak złagodzić ich obawy.

Dla jasności, jest to zupełnie nowy projekt, więc tak naprawdę nie chodzi o modyfikację kodu, aby umożliwić testowanie jednostkowe; chodzi o zaprojektowanie kodu, który napiszemy, aby był testowany jednostkowo.

Zawietrzny
źródło
33
Powtórzę: koledzy chcą testów jednostkowych dla nowego kodu, ale odmawiają napisania kodu w sposób umożliwiający jego testowanie jednostkowe, chociaż nie ma ryzyka zerwania czegokolwiek? Jeśli to prawda, powinieneś zaakceptować odpowiedź @ KilianFoth i poprosić go o wyróżnienie pierwszego zdania pogrubioną czcionką! Twoi koledzy najwyraźniej mają bardzo duże nieporozumienie na temat swojej pracy.
Doc Brown
20
@Lee: Kto powiedział, że odsprzężenie jest zawsze dobrym pomysłem? Czy widziałeś kiedyś bazę kodu, w której wszystko jest przekazywane jako interfejs utworzony z fabryki interfejsów przy użyciu interfejsu konfiguracji? Mam; został napisany w Javie i był to kompletny, niemożliwy do utrzymania, błędny bałagan. Ekstremalne oddzielenie to zaciemnianie kodu.
Christian Hackl
8
Efektywna praca Michaela Feathersa z Legacy Code radzi sobie bardzo dobrze z tym problemem i powinna dać ci dobry pomysł na temat zalet testowania nawet w nowej bazie kodu.
l0b0
8
@ l0b0 To jest w zasadzie biblia. W przypadku wymiany stosów nie byłaby to odpowiedź na pytanie, ale w RL powiedziałbym OP, aby przeczytał tę książkę (przynajmniej częściowo). OP, uzyskaj Efektywną pracę ze Starszym Kodem i przeczytaj go, przynajmniej częściowo (lub powiedz szefowi, żeby go dostał). Odpowiada na takie pytania. Zwłaszcza jeśli nie przeprowadzałeś testów, a teraz się w to angażujesz - możesz mieć 20 lat doświadczenia, ale teraz będziesz robić rzeczy, z którymi nie masz doświadczenia . O wiele łatwiej jest o nich przeczytać niż starannie nauczyć się tego wszystkiego metodą prób i błędów.
R. Schmitz
4
Dzięki za rekomendację książki Michaela Feathersa na pewno wybiorę kopię.
Lee

Odpowiedzi:

204

Niechęć do modyfikowania kodu w celu testowania pokazuje, że programista nie zrozumiał roli testów, a co za tym idzie ich własnej roli w organizacji.

Działalność związana z oprogramowaniem koncentruje się na dostarczaniu bazy kodu, która tworzy wartość biznesową. Odkryliśmy, dzięki długiemu i gorzkiemu doświadczeniu, że nie możemy stworzyć takich baz kodu o nietypowych rozmiarach bez testowania. Dlatego zestawy testowe są integralną częścią biznesu.

Wielu programistów zwraca uwagę na tę zasadę, ale podświadomie nigdy jej nie akceptuje. Łatwo jest zrozumieć, dlaczego tak jest; świadomość, że nasza własna zdolność umysłowa nie jest nieskończona i jest zaskakująco ograniczona w obliczu ogromnej złożoności nowoczesnej bazy kodu, jest niepożądana i łatwo tłumiona lub racjonalizowana. Fakt, że kod testowy nie jest dostarczany klientowi, ułatwia uwierzyć, że jest on obywatelem drugiej kategorii i nie jest niezbędny w porównaniu z „niezbędnym” kodem biznesowym. Pomysł dodania kodu testowego do kodu biznesowego dla wielu wydaje się podwójnie obraźliwy.

Problem z uzasadnieniem tej praktyki wiąże się z faktem, że cały obraz tworzenia wartości w branży oprogramowania jest często rozumiany tylko przez osoby zajmujące wyższe stanowiska w hierarchii firmy, ale osoby te nie mają szczegółowego technicznego zrozumienia przepływ pracy związany z kodowaniem wymagany do zrozumienia, dlaczego nie można się pozbyć testowania. Dlatego są zbyt często uspokajane przez praktyków, którzy zapewniają ich, że testowanie może być ogólnie dobrym pomysłem, ale „jesteśmy elitarnymi programistami, którzy nie potrzebują takich kul”, lub „nie mamy teraz na to czasu” itp. itp. Fakt, że sukces biznesowy to gra liczbowa i unikanie długu technicznego, zapewnianie jakość itp. pokazuje swoją wartość tylko w dłuższej perspektywie, co oznacza, że ​​często są w tym przekonaniu bardzo szczerzy.

Krótko mówiąc: testowanie kodu jest istotną częścią procesu programowania, podobnie jak w innych dziedzinach (wiele mikroczipów jest zaprojektowanych z dużą częścią elementów tylko do celów testowych), ale bardzo łatwo jest przeoczyć bardzo dobre powody że. Nie wpadnij w tę pułapkę.

Kilian Foth
źródło
39
Twierdziłbym, że zależy to od rodzaju zmiany. Istnieje różnica między ułatwieniem testowania kodu a wprowadzeniem specyficznych dla testu haków, których NIGDY nie należy używać w produkcji. Osobiście uważam na to drugie, ponieważ Murphy ...
Matthieu M.
61
Testy jednostkowe często przerywają enkapsulację i sprawiają, że testowany kod jest bardziej złożony niż byłby wymagany (np. Poprzez wprowadzenie dodatkowych typów interfejsów lub dodanie flag). Jak zawsze w inżynierii oprogramowania, każda dobra praktyka i każda dobra reguła ma swój udział w zobowiązaniach. Ślepe wykonanie wielu testów jednostkowych może mieć szkodliwy wpływ na wartość biznesową, nie wspominając już o tym, że napisanie i utrzymanie testów kosztuje już czas i wysiłek. Z mojego doświadczenia wynika, że ​​testy integracji mają znacznie większy zwrot z inwestycji i mają tendencję do ulepszania architektury oprogramowania przy mniejszej liczbie kompromisów.
Christian Hackl
20
@ Lee Pewnie, ale musisz rozważyć, czy posiadanie określonego rodzaju testów uzasadnia wzrost złożoności kodu. Moje osobiste doświadczenie jest takie, że testy jednostkowe są doskonałym narzędziem do momentu, w którym wymagają fundamentalnych zmian konstrukcyjnych w celu dostosowania się do kpiny. Tam przechodzę na inny rodzaj testów. Pisanie testów jednostkowych kosztem uczynienia architektury znacznie bardziej złożoną, wyłącznie w celu przeprowadzenia testów jednostkowych, polega na obserwowaniu pępka.
Konrad Rudolph
21
@ChristianHackl dlaczego test jednostkowy przerwałby enkapsulację? Odkryłem, że w przypadku kodu, nad którym pracowałem, jeśli istnieje potrzeba dodania dodatkowej funkcjonalności w celu umożliwienia testowania, faktycznym problemem jest to, że funkcja, którą chcesz przetestować, wymaga refaktoryzacji, więc cała funkcjonalność jest taka sama poziom abstrakcji (różnice w poziomie abstrakcji zwykle powodują tę „potrzebę” dodatkowego kodu), przy czym kod niższego poziomu jest przenoszony do własnych (testowalnych) funkcji.
Baldrickk
29
@ChristianHackl Testy jednostkowe nigdy nie powinny przerywać enkapsulacji, jeśli próbujesz uzyskać dostęp do zmiennych prywatnych, chronionych lub lokalnych z testu jednostkowego, robisz to źle. Jeśli testujesz funkcjonalność foo, testujesz tylko, czy rzeczywiście działała, a nie, jeśli zmienna lokalna x jest pierwiastkiem kwadratowym z wejścia y w trzeciej iteracji drugiej pętli. Jeśli jakaś funkcja jest prywatna, niech tak będzie, i tak będziesz ją testował w sposób tranzytowy. jeśli jest naprawdę duży i prywatny? Jest to wada projektowa, ale prawdopodobnie nie jest możliwa nawet poza C i C ++ z oddzielną implementacją nagłówka.
opa
75

To nie jest tak proste, jak mogłoby się wydawać. Rozbijmy to.

  • Pisanie testów jednostkowych jest zdecydowanie dobrą rzeczą.

ALE!

  • Każda zmiana w kodzie może wprowadzić błąd. Dlatego zmiana kodu bez uzasadnionego powodu biznesowego nie jest dobrym pomysłem.

  • Twoje „bardzo cienkie” webapi nie wydają się najlepszym przykładem do testowania jednostkowego.

  • Zmiana kodu i testów w tym samym czasie jest złą rzeczą.

Sugerowałbym następujące podejście:

  1. Napisz testy integracyjne . Nie powinno to wymagać żadnych zmian kodu. Dostarczy ci podstawowych przypadków testowych i pozwoli ci sprawdzić, czy wszelkie dalsze zmiany kodu nie wprowadzają żadnych błędów.

  2. Upewnij się, że nowy kod jest testowalny i zawiera testy jednostkowe i integracyjne.

  3. Upewnij się, że Twój łańcuch CI uruchamia testy po kompilacjach i wdrożeniach.

Kiedy już skonfigurujesz te rzeczy, dopiero wtedy zacznij myśleć o refaktoryzacji starszych projektów pod kątem testowalności.

Mamy nadzieję, że wszyscy wyciągną wnioski z tego procesu i będą mieli dobry pomysł na to, gdzie testowanie jest najbardziej potrzebne, jak chcesz go ustrukturyzować i jaką wartość przynosi firmie.

EDYCJA : Od kiedy napisałem tę odpowiedź, PO wyjaśnił pytanie, aby pokazać, że mówią o nowym kodzie, a nie o modyfikacjach istniejącego kodu. Być może naiwnie pomyślałem „Czy testy jednostkowe są dobre?” argument został rozstrzygnięty kilka lat temu.

Trudno sobie wyobrazić, jakie zmiany kodu byłyby wymagane w testach jednostkowych, ale nie byłyby ogólną dobrą praktyką, jakiej byś chciał. Prawdopodobnie rozsądne byłoby zbadanie rzeczywistych zastrzeżeń, być może jest to styl testowania jednostkowego, któremu sprzeciwia się.

Ewan
źródło
12
To jest znacznie lepsza odpowiedź niż zaakceptowana. Brak równowagi w głosowaniu jest przerażający.
Konrad Rudolph
4
@Lee Test jednostkowy powinien przetestować jednostkę funkcjonalności , która może, ale nie musi odpowiadać klasie. Jednostkę funkcjonalności należy przetestować na interfejsie (w tym przypadku może to być interfejs API). Testowanie może uwidocznić zapachy projektowe i potrzebę zastosowania innego / większego poziomu. Zbuduj swoje systemy z małych elementów składanych, łatwiej będzie je uzasadnić i przetestować.
Wes Toleman
2
@KonradRudolph: Wydaje mi się, że przegapiłem punkt, w którym OP dodał, że to pytanie dotyczy zaprojektowania nowego kodu, a nie zmiany istniejącego. Więc nie ma nic do złamania, co sprawia, że ​​większość tej odpowiedzi nie ma zastosowania.
Doc Brown
1
Zdecydowanie nie zgadzam się ze stwierdzeniem, że pisanie testów jednostkowych jest zawsze dobrą rzeczą. Testy jednostkowe są dobre tylko w niektórych przypadkach. Głupio jest używać testów jednostkowych do testowania kodu interfejsu użytkownika, są one przeznaczone do testowania logiki biznesowej. Warto również napisać testy jednostkowe, aby zastąpić brakujące kontrole kompilacji (np. W Javascript). Większość kodu przeznaczonego tylko dla interfejsu użytkownika powinna pisać wyłącznie testy kompleksowe, a nie testy jednostkowe.
Sulthan
1
Wzory zdecydowanie mogą cierpieć z powodu „uszkodzeń wywołanych przez test”. Zwykle testowalność poprawia projekt: Podczas pisania testów zauważasz, że czegoś nie można pobrać, ale należy je przekazać, co zapewnia wyraźniejsze interfejsy i tak dalej. Ale od czasu do czasu natkniesz się na coś, co wymaga niewygodnego projektu tylko do testów. Przykładem może być konstruktor testowy wymagany w nowym kodzie ze względu na istniejący kod innej firmy, który używa na przykład singletonu. Kiedy tak się dzieje: cofnij się i wykonaj tylko test integracyjny, zamiast uszkadzać własny projekt w imię testowalności.
Anders Forsgren
18

Projektowanie kodu tak, aby był z natury testowalny, nie jest zapachem kodu; wręcz przeciwnie, jest to znak dobrego projektu. Istnieje kilka dobrze znanych i powszechnie używanych wzorców projektowych opartych na tym (np. Model-View-Presenter), które oferują łatwe (łatwiejsze) testowanie jako dużą zaletę.

Tak więc, jeśli musisz napisać interfejs dla konkretnej klasy, aby łatwiej go przetestować, to dobrze. Jeśli masz już konkretną klasę, większość IDE może wyodrębnić z niej interfejs, dzięki czemu nakład pracy jest minimalny. Synchronizacja tych dwóch elementów jest nieco większa, ale interfejs i tak nie powinien się wiele zmieniać, a korzyści z testowania mogą przeważyć ten dodatkowy wysiłek.

Z drugiej strony, jak @ MatthieuM. wspomniane w komentarzu, jeśli dodajesz do kodu określone punkty wejścia, które nigdy nie powinny być używane w produkcji, tylko ze względu na testy, może to stanowić problem.

mmathis
źródło
Problem ten można rozwiązać za pomocą statycznej analizy kodu - oznacz metody (np. Należy je nazwać _ForTest) i sprawdź bazę kodów pod kątem wywołań z kodu nie testowego.
Riking
13

IMHO bardzo łatwo jest zrozumieć, że do tworzenia testów jednostkowych testowany kod musi mieć przynajmniej pewne właściwości. Na przykład, jeśli kod nie składa się z pojedynczych jednostek, które można testować osobno, słowo „testowanie jednostkowe” nawet nie zaczyna mieć sensu. Jeśli kod nie ma tych właściwości, należy go najpierw zmienić, co jest dość oczywiste.

Powiedział, że teoretycznie można najpierw spróbować napisać testowalną jednostkę kodu, stosując wszystkie zasady SOLID, a następnie spróbować napisać dla niej test, bez dalszej modyfikacji oryginalnego kodu. Niestety, pisanie kodu, który jest naprawdę testowalny jednostkowo, nie zawsze jest bardzo proste, więc jest całkiem prawdopodobne, że konieczne będą pewne zmiany, które można wykryć tylko podczas próby utworzenia testów. Odnosi się to do kodu, nawet gdy został napisany z myślą o testowaniu jednostkowym, i zdecydowanie bardziej dotyczy kodu, który został napisany tam, gdzie „testowanie jednostkowe” nie było na początku na porządku dziennym.

Istnieje dobrze znane podejście, które próbuje rozwiązać problem, pisząc najpierw testy jednostkowe - nazywa się to Test Driven Development (TDD), a na pewno może pomóc w zwiększeniu możliwości testowania kodu od samego początku.

Oczywiście niechęć do późniejszego zmieniania kodu w celu umożliwienia jego przetestowania pojawia się często w sytuacji, gdy kod został najpierw przetestowany ręcznie i / lub działa dobrze w produkcji, więc zmiana go może faktycznie wprowadzić nowe błędy, to prawda. Najlepszym sposobem na złagodzenie tego problemu jest utworzenie najpierw zestawu testów regresji (który często można wdrożyć przy bardzo minimalnych zmianach w podstawie kodu), a także innych towarzyszących środków, takich jak recenzje kodu lub nowe ręczne sesje testowe. To powinno dać ci wystarczającą pewność siebie, aby przeprojektowanie niektórych elementów wewnętrznych nie zepsuło niczego ważnego.

Doktor Brown
źródło
Ciekawe, że wspominasz TDD. Próbujemy wprowadzić BDD / TDD, które również spotkało się z pewnym oporem - a mianowicie, co tak naprawdę oznacza „minimalny kod do przekazania”.
Lee
2
@Lee: wprowadzanie zmian w organizacji zawsze powoduje pewien opór i zawsze potrzebuje trochę czasu na dostosowanie nowych rzeczy, to nie jest nowa mądrość. To jest problem ludzi.
Doc Brown
Absolutnie. Chciałbym mieć więcej czasu!
Lee
Często chodzi o pokazanie ludziom, że zrobienie tego w ten sposób zaoszczędzi im czasu (i miejmy nadzieję, że również szybko). Po co robić coś, co nie przynosi korzyści?
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen: Zespół może również pokazać PO, że jego podejście pozwoli zaoszczędzić czas. Kto wie? Zastanawiam się jednak, czy nie mamy tutaj do czynienia z problemami mniej technicznymi; OP wciąż przychodzi tutaj, aby powiedzieć nam, co jego drużyna robi źle (jego zdaniem), tak jakby próbował znaleźć sojuszników dla swojej sprawy. Bardziej korzystne może być omówienie projektu wspólnie z zespołem, a nie z nieznajomymi na Stack Exchange.
Christian Hackl
11

Mam problem z (bezpodstawnym) stwierdzeniem, które czynisz:

aby przetestować jednostkę Web API, będziemy musieli kpić z tej fabryki

To niekoniecznie prawda. Istnieje wiele sposobów pisania testów i sposoby pisania testów jednostkowych, które nie obejmują próbnych prób. Co ważniejsze, istnieją inne rodzaje testów, takie jak testy funkcjonalne lub integracyjne. Wiele razy można znaleźć „szew testowy” w „interfejsie”, który nie jest językiem programowania OOP interface.

Kilka pytań, które pomogą Ci znaleźć alternatywny szew testowy, który może być bardziej naturalny:

  • Czy kiedykolwiek będę chciał napisać cienki interfejs API sieci Web na innym interfejsie API?
  • Czy mogę zmniejszyć duplikację kodu między interfejsem API sieci Web a interfejsem API bazowym? Czy jedno można wygenerować w kategoriach drugiego?
  • Czy mogę traktować cały interfejs API sieci Web i podstawowy interfejs API jako pojedynczą „czarną skrzynkę” i w znaczący sposób twierdzić, jak się zachowuje całość?
  • Gdyby interfejs API sieci Web musiał zostać w przyszłości zastąpiony nową implementacją, jak byśmy to zrobili?
  • Jeśli interfejs API sieci Web zostanie w przyszłości zastąpiony nową implementacją, czy klienci interfejsu API sieci Web będą w stanie to zauważyć? Jeśli tak to jak?

Kolejne bezpodstawne stwierdzenie, które czynisz, dotyczy DI:

albo projektujemy klasę kontrolera interfejsu API sieci Web, aby akceptować DI (za pośrednictwem jego konstruktora lub programu ustawiającego), co oznacza, że ​​projektujemy część kontrolera tylko po to, aby umożliwić DI i implementować interfejs, którego inaczej nie potrzebujemy, lub korzystamy z usług strony trzeciej frameworku takim jak Ninject, aby uniknąć konieczności zaprojektowania kontrolera w ten sposób, ale nadal będziemy musieli stworzyć interfejs.

Wstrzykiwanie zależności niekoniecznie oznacza tworzenie nowego interface. Na przykład w przypadku tokena uwierzytelniającego: czy można po prostu programowo utworzyć prawdziwy token uwierzytelniający? Następnie test może utworzyć takie tokeny i wstrzyknąć je. Czy proces sprawdzania tokena zależy od pewnego rodzaju tajemnicy kryptograficznej? Mam nadzieję, że nie zakodowałeś sekretu - spodziewam się, że w jakiś sposób możesz go odczytać z pamięci, aw takim przypadku możesz po prostu użyć innego (dobrze znanego) sekretu w testach.

Nie oznacza to, że nigdy nie powinieneś tworzyć nowego interface. Ale nie skupiaj się na tym, że istnieje tylko jeden sposób na napisanie testu lub jeden sposób na sfałszowanie zachowania. Jeśli myślisz nieszablonowo, zwykle możesz znaleźć rozwiązanie, które będzie wymagało minimum zniekształceń kodu, a jednocześnie da ci pożądany efekt.

Daniel Pryden
źródło
Zwróciliśmy uwagę na twierdzenia dotyczące interfejsów, ale nawet gdybyśmy ich nie użyli, nadal musielibyśmy jakoś wstrzykiwać obiekty, jest to troska reszty zespołu. tzn. niektórzy w zespole byliby zadowoleni z bez parametrów parametrycznego tworzenia instancji konkretnej implementacji i pozostawiania jej przy tym. W rzeczywistości jeden członek zgłosił pomysł użycia refleksji do wstrzykiwania fałszywych prób, abyśmy nie musieli projektować kodu, aby je zaakceptować. Który to śmierdzący zapachowy imo
Lee
9

Masz szczęście, ponieważ jest to nowy projekt. Przekonałem się, że Test Driven Design działa bardzo dobrze do pisania dobrego kodu (dlatego właśnie to robimy).

Rozumiejąc z góry, jak wywołać dany fragment kodu przy użyciu realistycznych danych wejściowych, a następnie uzyskać realistyczne dane wyjściowe, które można sprawdzić zgodnie z przeznaczeniem, opracowujesz interfejs API na bardzo wczesnym etapie procesu i masz dużą szansę na uzyskanie przydatny projekt, ponieważ nie przeszkadza ci istniejący kod, który trzeba przepisać, aby go dostosować. Równie łatwo jest to zrozumieć swoim rówieśnikom, dzięki czemu możesz ponownie odbyć dobre dyskusje na wczesnym etapie procesu.

Zauważ, że „użyteczne” w powyższym zdaniu oznacza nie tylko to, że uzyskane metody są łatwe do wywołania, ale także że masz tendencję do uzyskiwania czystych interfejsów, które są łatwe do skonfigurowania w testach integracyjnych, i do pisania makiet.

Rozważ to. Zwłaszcza z recenzją. Z mojego doświadczenia wynika, że ​​inwestycja czasu i wysiłku bardzo szybko się zwróci.

Thorbjørn Ravn Andersen
źródło
Mamy również problem z TDD, a mianowicie, co stanowi „minimalny kod do przekazania”. Pokazałem zespołowi ten proces, a oni wzięli wyjątek, nie tylko pisząc to, co już zaprojektowaliśmy - co rozumiem. „Minimum” nie wydaje się być zdefiniowane. Jeśli piszemy test i mamy jasne plany i projekty, dlaczego nie napisać tego, aby przejść test?
Lee
@ Patrz „minimalny kod do przekazania”… cóż, może to zabrzmieć trochę głupio, ale to dosłownie to, co mówi. Np. Jeśli masz test UserCanChangeTheirPassword, wówczas w teście wywołujesz funkcję (jeszcze nieistniejącą), aby zmienić hasło, a następnie stwierdzasz, że hasło rzeczywiście zostało zmienione. Następnie piszesz funkcję, dopóki nie możesz uruchomić testu, który nie generuje wyjątków ani nie ma błędnego potwierdzenia. Jeśli w tym momencie masz powód, aby dodać dowolny kod, powód ten przechodzi do kolejnego testu, np UserCantChangePasswordToEmptyString.
R. Schmitz
@Lee Ostatecznie Twoje testy będą dokumentacją tego, co robi Twój kod, z wyjątkiem dokumentacji, która sprawdza, czy sam się spełnia, a nie tylko tuszem na papierze. Porównaj również z tym pytaniem - metoda, CalculateFactorialktóra zwraca 120 i test kończy się pomyślnie. To jest minimum. Oczywiście nie jest to również zamierzone, ale oznacza to po prostu, że potrzebujesz kolejnego testu, aby wyrazić to, co było zamierzone.
R. Schmitz
1
@Lee Małe kroki. Bezwzględne minimum może być więcej niż myślisz, gdy kod wzrośnie powyżej trywialnego. Również projekt, który wykonujesz, wdrażając całość na raz, może być mniej optymalny, ponieważ przyjmujesz założenia, jak to zrobić, nie pisząc jeszcze testów, które to pokazują. Ponownie pamiętaj, że kod powinien początkowo zawieść.
Thorbjørn Ravn Andersen
1
Również testy regresji są bardzo ważne. Czy są w zasięgu zespołu?
Thorbjørn Ravn Andersen
8

Jeśli musisz zmodyfikować kod, będzie to zapach kodu.

Z własnego doświadczenia, jeśli mój kod jest trudny do napisania testów, jest to zły kod. To nie jest zły kod, ponieważ nie działa ani nie działa zgodnie z przeznaczeniem, jest zły, ponieważ nie mogę szybko zrozumieć, dlaczego działa. Jeśli napotkam błąd, wiem, że naprawienie go będzie wymagało długiej i bolesnej pracy. Kod jest również trudny / niemożliwy do ponownego użycia.

Dobry (czysty) kod dzieli zadania na mniejsze sekcje, które są łatwe do zrozumienia na pierwszy rzut oka (lub przynajmniej dobry wygląd). Testowanie tych mniejszych sekcji jest łatwe. Mogę również pisać testy, które testują tylko fragment bazy kodu z podobną łatwością, jeśli jestem dość pewny co do podsekcji (ponowne użycie również pomaga tutaj, ponieważ zostało już przetestowane).

Od samego początku kod powinien być łatwy do przetestowania, refaktoryzacji i ponownego wykorzystania, a sam nie zabijesz się, gdy będziesz musiał wprowadzić zmiany.

Piszę to podczas kompletnej przebudowy projektu, który powinien być prototypem jednorazowego użytku, w czystszy kod. O wiele lepiej jest to zrobić od samego początku i jak najszybciej poprawić zły kod, zamiast godzinami wpatrywać się w ekran, bojąc się dotknąć czegokolwiek ze strachu przed uszkodzeniem czegoś, co częściowo działa.

David
źródło
3
„Prototypowy rzut” - każdy projekt rozpoczyna życie jako jeden z… najlepiej myśleć o rzeczach, które nigdy nie są. pisząc to jak ja .. zgadnij co? ... refaktoryzuje prototyp, który okazał się nie być;)
Algy Taylor
4
Jeśli chcesz mieć pewność, że wyrzucony prototyp zostanie wyrzucony, napisz go w języku prototypowym, który nigdy nie będzie dozwolony w produkcji. Clojure i Python to dobry wybór.
Thorbjørn Ravn Andersen
2
@ ThorbjørnRavnAndersen To mnie rozśmieszyło. Czy to miało być kopanie w tych językach? :)
Lee
@Zawietrzny. Nie, tylko przykłady języków, które mogą być nieakceptowalne w produkcji - zazwyczaj dlatego, że nikt w organizacji nie może ich utrzymywać, ponieważ nie znają ich, a ich krzywe uczenia się są strome. Jeśli są dopuszczalne, wybierz inną, która nie jest.
Thorbjørn Ravn Andersen
4

Twierdziłbym, że pisanie kodu, którego nie można przetestować jednostkowo, to zapach kodu. Ogólnie rzecz biorąc, jeśli twój kod nie może być testowany jednostkowo, to nie jest modułowy, co utrudnia zrozumienie, utrzymanie lub ulepszenie. Może jeśli kod jest kodem klejącym, który naprawdę ma sens tylko w kontekście testów integracyjnych, możesz zastąpić testowanie integracyjne testowaniem jednostkowym, ale nawet wtedy, gdy integracja się nie powiedzie, będziesz musiał wyizolować problem, a testowanie jednostkowe to świetny sposób na Zrób to.

Mówisz

Planujemy utworzenie fabryki, która zwróci typ metody uwierzytelniania. Nie musimy dziedziczyć interfejsu, ponieważ nie spodziewamy się, że będzie to coś innego niż konkretny typ. Aby jednak przetestować usługę Web API, będziemy musieli kpić z tej fabryki.

Tak naprawdę nie podążam za tym. Powodem posiadania fabryki, która coś tworzy, jest umożliwienie zmiany fabryk lub zmiany tego, co fabryka tworzy z łatwością, aby inne części kodu nie musiały się zmieniać. Jeśli twoja metoda uwierzytelniania nigdy się nie zmieni, fabryka jest bezużyteczna. Jednak jeśli chcesz mieć inną metodę uwierzytelniania w teście niż w produkcji, doskonałym rozwiązaniem jest posiadanie fabryki, która zwraca inną metodę uwierzytelnienia w teście niż w produkcji.

Nie potrzebujesz do tego DI lub Mocks. Potrzebujesz tylko fabryki, aby obsługiwała różne typy uwierzytelniania i aby mogła być w jakiś sposób konfigurowalna, na przykład z pliku konfiguracyjnego lub zmiennej środowiskowej.

Old Pro
źródło
2

W każdej dyscyplinie inżynierskiej, o której myślę, jest tylko jeden sposób na osiągnięcie przyzwoitego lub wyższego poziomu jakości:

Aby uwzględnić kontrolę / testowanie w projekcie.

Dotyczy to budownictwa, projektowania układów, opracowywania oprogramowania i produkcji. Nie oznacza to jednak, że testowanie jest filarem, przy którym każdy projekt musi być zbudowany, w ogóle. Jednak przy każdej decyzji projektowej projektanci muszą mieć jasność co do wpływu na koszty testowania i podejmować świadome decyzje dotyczące kompromisu.

W niektórych przypadkach ręczne lub zautomatyzowane (np. Selen) testowanie będzie wygodniejsze niż testy jednostkowe, a jednocześnie same w sobie zapewnią akceptowalny zakres testów. W rzadkich przypadkach można też wyrzucić coś, co jest prawie całkowicie niesprawdzone. Ale muszą to być świadome indywidualne decyzje. Wywołanie projektu uwzględniającego testowanie „zapachu kodu” oznacza poważny brak doświadczenia.

Piotr
źródło
1

Odkryłem, że testy jednostkowe (i inne rodzaje testów automatycznych) mają tendencję do zmniejszania zapachów kodu i nie mogę wymyślić żadnego przykładu, w którym wprowadzają zapachy kodu. Testy jednostkowe zwykle zmuszają cię do pisania lepszego kodu. Jeśli nie możesz łatwo zastosować testowanej metody, dlaczego powinno być łatwiej w twoim kodzie?

Dobrze napisane testy jednostkowe pokazują, w jaki sposób kod ma być używany. Są formą dokumentacji wykonywalnej. Widzę ohydnie napisane, zbyt długie testy jednostkowe, których po prostu nie można zrozumieć. Nie pisz tego! Jeśli musisz pisać długie testy, aby skonfigurować swoje klasy, Twoje klasy wymagają refaktoryzacji.

Testy jednostkowe podkreślą, gdzie znajdują się niektóre zapachy kodu. Radziłbym przeczytać Efektywną współpracę Michaela C. Feathersa z kodem Legacy . Mimo że Twój projekt jest nowy, jeśli nie ma już żadnych (lub wielu) testów jednostkowych, możesz potrzebować kilku nieoczywistych technik, aby Twój kod ładnie przetestował.

CJ Dennis
źródło
3
Możesz ulec pokusie wprowadzenia wielu warstw pośrednich, aby móc testować, a następnie nigdy nie używaj ich zgodnie z oczekiwaniami.
Thorbjørn Ravn Andersen
1

W skrócie:

Testowalny kod to (zwykle) kod, który można utrzymać - a raczej kod trudny do przetestowania jest zwykle trudny do utrzymania. Projektowanie kodu, który nie jest testowalny, przypomina projektowanie maszyny, której nie da się naprawić - szkoda, że ​​biedny shmuck, który zostanie przydzielony do ostatecznej naprawy (może to być ty).

Jednym z przykładów jest to, że planujemy stworzyć fabrykę, która zwróci typ metody uwierzytelnienia. Nie musimy dziedziczyć interfejsu, ponieważ nie spodziewamy się, że będzie to coś innego niż konkretny typ.

Wiesz, że będziesz potrzebować pięciu różnych typów metod uwierzytelniania za trzy lata, teraz, kiedy to powiedziałeś, prawda? Wymagania się zmieniają i chociaż powinieneś unikać nadmiernej inżynierii swojego projektu, posiadanie testowalnego projektu oznacza, że ​​twój projekt ma (tylko) wystarczającą liczbę szwów, aby mógł zostać zmieniony bez (zbyt dużego) bólu - i że testy modułowe zapewnią ci zautomatyzowane środki, aby to zobaczyć twoje zmiany niczego nie psują.

CharonX
źródło
1

Projektowanie wokół wstrzykiwania zależności nie jest zapachem kodu - to najlepsza praktyka. Używanie DI to nie tylko testowalność. Budowanie komponentów wokół DI ułatwia modułowość i możliwość ponownego użycia, łatwiej pozwala na zamianę głównych komponentów (takich jak warstwa interfejsu bazy danych). Mimo że dodaje pewnego stopnia złożoności, właściwie wykonane, pozwala na lepsze oddzielanie warstw i izolację funkcjonalności, co sprawia, że ​​złożoność jest łatwiejsza w zarządzaniu i nawigacji. Ułatwia to prawidłową weryfikację zachowania każdego komponentu, zmniejszając liczbę błędów, a także ułatwia śledzenie błędów.

Zenilogix
źródło
1
„zrobione dobrze” to problem. Muszę utrzymywać dwa projekty, w których DI został źle wykonany (chociaż jego celem było zrobienie tego „dobrze”). To sprawia, że ​​kod jest okropny i znacznie gorszy niż starsze projekty bez DI i testów jednostkowych. Poprawne DI nie jest łatwe.
Jan
@Jan, to interesujące. Jak zrobili to źle?
Lee
1
Projekt @Lee One jest usługą, która wymaga szybkiego czasu uruchomienia, ale jest strasznie powolna na starcie, ponieważ cała inicjalizacja klasy odbywa się z góry przez środowisko DI (Castle Windsor w C #). Innym problemem, który widzę w tych projektach, jest mieszanie DI z tworzeniem obiektów z „nowym”, omijaniem DI. To sprawia, że ​​testowanie jest trudne i prowadziło do nieprzyjemnych warunków wyścigowych.
Jan
1

Zasadniczo oznacza to, że albo projektujemy klasę kontrolera Web API, aby akceptować DI (przez jego konstruktora lub settera), co oznacza, że ​​projektujemy część kontrolera tylko po to, aby umożliwić DI i implementację interfejsu, którego w innym przypadku nie potrzebujemy, lub używamy frameworkiem zewnętrznym, takim jak Ninject, aby uniknąć konieczności zaprojektowania kontrolera w ten sposób, ale nadal będziemy musieli stworzyć interfejs.

Spójrzmy na różnicę między testowalnymi:

public class MyController : Controller
{
    private readonly IMyDependency _thing;

    public MyController(IMyDependency thing)
    {
        _thing = thing;
    }
}

i nie-testowalny kontroler:

public class MyController : Controller
{
}

Pierwsza opcja zawiera dosłownie 5 dodatkowych wierszy kodu, z których dwa mogą być automatycznie generowane przez Visual Studio. Po skonfigurowaniu struktury wstrzykiwania zależności w celu zastąpienia konkretnego typu IMyDependencyw czasie wykonywania - która dla każdego przyzwoitego frameworka DI jest kolejnym pojedynczym wierszem kodu - wszystko po prostu działa, z wyjątkiem tego, że teraz możesz wyśmiewać, a tym samym przetestować kontroler pod kątem zadowolenia .

6 dodatkowych wierszy kodu, aby umożliwić testowanie ... a twoi koledzy twierdzą, że to „za dużo pracy”? Ten argument nie leci ze mną i nie powinien latać z tobą.

I nie musisz tworzyć i implementować interfejsu do testowania: Moq , na przykład, pozwala symulować zachowanie konkretnego typu do celów testowania jednostkowego. Oczywiście nie przyda ci się to, jeśli nie możesz wstrzyknąć tego typu do testowanych klas.

Wstrzykiwanie zależności jest jedną z tych rzeczy, które po zrozumieniu zastanawiają się „jak ja bez tego pracowałem?”. To proste, skuteczne i po prostu robi sens. Nie pozwól, aby niezrozumienie przez twoich kolegów nowych rzeczy utrudniało testowanie twojego projektu.

Ian Kemp
źródło
1
To, co tak szybko odrzucasz, jak „brak zrozumienia nowych rzeczy”, może okazać się dobrym zrozumieniem starych rzeczy. Zastrzyk uzależnienia z pewnością nie jest nowy. Pomysł i prawdopodobnie najwcześniejsze wdrożenia mają już kilkadziesiąt lat. I tak, uważam, że twoja odpowiedź jest przykładem skomplikowania kodu z powodu testów jednostkowych i być może przykładem testów jednostkowych przerywających enkapsulację (ponieważ kto twierdzi, że klasa ma konstruktor publiczny?). Często usuwałem zastrzyk zależności z baz kodu, które odziedziczyłem od kogoś innego, z powodu kompromisów.
Christian Hackl
Kontrolery zawsze mają konstruktor publiczny, niejawny lub nie, ponieważ wymaga tego MVC. „Skomplikowane” - może, jeśli nie rozumiesz, jak działają konstruktory. Hermetyzacja - tak, w niektórych przypadkach, ale debata na temat DI w enkapsulacji jest ciągłą, bardzo subiektywną dyskusją, która tutaj nie pomoże, a zwłaszcza w przypadku większości aplikacji, DI będzie służyć lepiej niż IMO enkapsulacji.
Ian Kemp
W odniesieniu do konstruktorów publicznych: jest to szczególna specyfika wykorzystywanego frameworka. Myślałem o bardziej ogólnym przypadku zwykłej klasy, który nie jest tworzony przez framework. Dlaczego uważasz, że postrzeganie dodatkowych parametrów metody jako dodatkowej złożoności oznacza brak zrozumienia na temat działania konstruktorów? Doceniam jednak fakt, że uznajesz istnienie kompromisu między DI a enkapsulacją.
Christian Hackl
0

Kiedy piszę testy jednostkowe, zaczynam myśleć o tym, co może pójść nie tak w moim kodzie. Pomaga mi to ulepszyć projektowanie kodu i zastosować zasadę pojedynczej odpowiedzialności (SRP). Ponadto, kiedy wrócę, aby zmodyfikować ten sam kod kilka miesięcy później, pomaga mi potwierdzić, że istniejąca funkcjonalność nie jest zepsuta.

Istnieje tendencja do wykorzystywania czystych funkcji w jak największym stopniu (aplikacje bezserwerowe). Testy jednostkowe pomagają mi izolować stan i pisać czyste funkcje.

W szczególności będziemy mieć usługę interfejsu API sieci Web, która będzie bardzo cienka. Jego głównym zadaniem będzie zbieranie żądań / odpowiedzi internetowych i wywoływanie bazowego interfejsu API zawierającego logikę biznesową.

Najpierw napisz testy jednostkowe dla bazowego API, a jeśli masz wystarczająco dużo czasu na opracowanie, musisz również napisać testy dla cienkiej usługi Web API.

TL; DR, testowanie jednostkowe pomaga poprawić jakość kodu i pomaga wprowadzić przyszłe zmiany w kodzie bez ryzyka. Poprawia także czytelność kodu. Wykorzystaj testy zamiast komentarzy, aby przedstawić swój punkt widzenia.

Ashutosh
źródło
0

Podsumowując, i co powinno być twoim argumentem wobec niechętnego losu, jest to, że nie ma konfliktu. Dużym błędem wydaje się być to, że ktoś wymyślił pomysł „zaprojektowania do testowania” osobom, które nie znoszą testowania. Powinni po prostu zamknąć usta lub wyrazić to inaczej, na przykład „nie spieszmy się, aby zrobić to dobrze”.

Pomysł, że „musisz wdrożyć interfejs”, aby umożliwić testowanie czegoś, jest błędny. Interfejs jest już zaimplementowany, po prostu nie jest jeszcze zadeklarowany w deklaracji klasy. Chodzi o rozpoznanie istniejących metod publicznych, skopiowanie ich podpisów do interfejsu i zadeklarowanie tego interfejsu w deklaracji klasy. Bez programowania, bez zmian w istniejącej logice.

Najwyraźniej niektórzy mają inne zdanie na ten temat. Sugeruję, aby najpierw spróbować to naprawić.

Martin Maat
źródło