W przypadku niektórych najpopularniejszych języków (Java, C #, Java itp.) Czasami wydaje się, że pracujesz w sprzeczności z językiem, gdy chcesz w pełni TDD kodu.
Na przykład w Javie i języku C # będziesz chciał wyśmiewać wszelkie zależności swoich klas, a większość frameworkowców zaleci, byś wyśmiewał interfejsy, a nie klasy. Często oznacza to, że masz wiele interfejsów z jedną implementacją (ten efekt jest jeszcze bardziej zauważalny, ponieważ TDD zmusi cię do napisania większej liczby mniejszych klas). Rozwiązania, które pozwalają poprawnie kpić z konkretnych klas, zmieniają kompilator lub zastępują moduły ładujące klasy itp., Co jest dość nieprzyjemne.
Jak więc wyglądałby język, gdyby został zaprojektowany od zera, aby był świetny dla TDD? Być może jakiś sposób opisywania zależności na poziomie języka (zamiast przekazywania interfejsów do konstruktora) i możliwość oddzielenia interfejsu klasy bez robienia tego wprost?
Odpowiedzi:
Wiele lat temu rzuciłem prototyp, który dotyczył podobnego pytania; oto zrzut ekranu:
Pomysł polegał na tym, że twierdzenia są zgodne z samym kodem, a wszystkie testy uruchamiane są w zasadzie przy każdym naciśnięciu klawisza. Gdy tylko zdasz test, zobaczysz, że metoda zmienia kolor na zielony.
źródło
Byłoby to dynamiczne, a nie statyczne. Pisanie kaczką wykona wtedy tę samą pracę, co interfejsy w statycznych językach. Ponadto jego klasy można modyfikować w czasie wykonywania, aby środowisko testowe mogło z łatwością wprowadzać lub wyszydzać metody z istniejących klas. Ruby jest jednym z takich języków; rspec to najlepsza platforma testowa dla TDD.
Jak dynamiczne pisanie pomaga testować
Dzięki dynamicznemu pisaniu możesz tworzyć fałszywe obiekty, po prostu tworząc klasę, która ma ten sam interfejs (podpisy metod), obiekt współpracujący, którego potrzebujesz wyśmiewać. Załóżmy na przykład, że masz klasę, która wysłała wiadomości:
Powiedzmy, że mamy MessageSenderUser, który korzysta z instancji MessageSender:
Zwróć uwagę na zastosowanie zastrzyku zależności , podstawowego zestawu testów jednostkowych. Wrócimy do tego.
Chcesz sprawdzić, czy
MessageSenderUser#do_stuff
połączenia wysyłane są dwukrotnie. Podobnie jak w przypadku języka o typie statycznym, możesz utworzyć próbny MessageSender, który zlicza liczbę wywołańsend
. Jednak w przeciwieństwie do języka o typie statycznym nie potrzebujesz klasy interfejsu. Po prostu idź i stwórz go:I użyj go w swoim teście:
Samo „pisanie kaczką” w języku dynamicznie wpisywanym nie dodaje zbyt wiele do testowania w porównaniu do języka statycznego. Ale co, jeśli klasy nie są zamknięte, ale można je modyfikować w czasie wykonywania? To zmieniacz gier. Zobaczmy jak.
Co się stanie, jeśli nie będziesz musiał używać wstrzykiwania zależności, aby przetestować klasę?
Załóżmy, że MessageSenderUser będzie kiedykolwiek używał MessageSender tylko do wysyłania wiadomości, a Ty nie musisz zezwalać na zastępowanie MessageSender przez inną klasę. W jednym programie często tak jest. Przepiszmy MessageSenderUser, aby po prostu tworzył i używał MessageSender, bez wstrzykiwania zależności.
MessageSenderUser jest teraz prostszy w użyciu: nikt go tworzący nie musi tworzyć MessageSender, aby mógł z niego korzystać. W tym prostym przykładzie nie wygląda to na duże ulepszenie, ale teraz wyobraź sobie, że MessageSenderUser jest tworzony w więcej niż jednym miejscu lub że ma trzy zależności. Teraz system ma wiele instancji, aby uszczęśliwić testy jednostkowe, nie dlatego, że w ogóle poprawia projekt.
Klasy otwarte umożliwiają testowanie bez wstrzykiwania zależności
Struktura testowa w języku z dynamicznym pisaniem i otwartymi klasami może uczynić TDD całkiem niezłym. Oto fragment kodu z testu rspec dla MessageSenderUser:
To cały test. Jeśli
MessageSenderUser#do_stuff
nie wywołaMessageSender#send
dokładnie dwa razy, test nie powiedzie się. Prawdziwa klasa MessageSender nigdy nie jest wywoływana: powiedzieliśmy testowi, że ilekroć ktoś próbuje utworzyć MessageSender, powinien zamiast tego uzyskać nasz pozorowany MessageSender. Nie jest konieczne wstrzykiwanie zależności.Miło jest robić tyle w tak prostszym teście. Coraz przyjemniej jest nie stosować zastrzyku zależności, chyba że ma to sens dla twojego projektu.
Ale co to ma wspólnego z otwartymi zajęciami? Zanotuj połączenie z
MessageSender.should_receive
. Nie pisaliśmy #should_receive, kiedy pisaliśmy MessageSender, więc kto to zrobił? Odpowiedź jest taka, że struktura testowa, dokonująca pewnych ostrożnych modyfikacji klas systemowych, może sprawić, że będzie wyglądać tak, jakby #should_receive było zdefiniowane dla każdego obiektu. Jeśli uważasz, że modyfikowanie takich klas systemowych wymaga pewnej ostrożności, masz rację. Ale jest to idealna rzecz do tego, co robi biblioteka testowa, a otwarte klasy umożliwiają to.źródło
„działa dobrze z TDD” z pewnością nie wystarcza do opisania języka, więc może „wyglądać” jak cokolwiek innego. Lisp, Prolog, C ++, Ruby, Python ... wybierz coś dla siebie.
Co więcej, nie jest jasne, czy obsługa TDD jest czymś, co najlepiej obsłużyć sam język. Jasne, możesz stworzyć język, w którym każda funkcja lub metoda ma powiązany test, i możesz wbudować obsługę wykrywania i wykonywania tych testów. Jednak platformy do testowania jednostkowego już dobrze radzą sobie z częścią dotyczącą wykrywania i wykonywania i trudno jest zobaczyć, jak dokładnie dodać wymaganie testu dla każdej funkcji. Czy testy również wymagają testów? Czy są dwie klasy funkcji - normalne, które wymagają testów i funkcje testowe, które ich nie potrzebują? To nie wydaje się zbyt eleganckie.
Może lepiej jest obsługiwać TDD za pomocą narzędzi i struktur. Zbuduj go w IDE. Stwórz proces rozwoju, który go zachęci.
Ponadto, jeśli projektujesz język, dobrze jest myśleć długoterminowo. Pamiętaj, że TDD to tylko jedna metodologia, a nie preferowany przez wszystkich sposób pracy. Trudno to sobie wyobrazić, ale możliwe, że nadejdą jeszcze lepsze sposoby. Czy jako projektant języków chcesz, aby ludzie musieli porzucić swój język, kiedy to się stanie?
Aby odpowiedzieć na to pytanie, możesz powiedzieć, że taki język sprzyjałby testowaniu. Wiem, że to niewiele pomaga, ale myślę, że problem dotyczy pytania.
źródło
Cóż, dynamicznie pisane języki nie wymagają jawnych interfejsów. Zobacz Ruby lub PHP itp.
Z drugiej strony, statycznie pisane języki, takie jak Java i C # lub C ++, wymuszają typy i zmuszają do pisania tych interfejsów.
Nie rozumiem, z czym jest twój problem. Interfejsy są kluczowym elementem projektowania i są używane we wszystkich wzorach projektowych oraz w poszanowaniu zasad SOLID. Na przykład często używam interfejsów w PHP, ponieważ jawnie projektują i wymuszają projektowanie. Z drugiej strony w Ruby nie ma sposobu na wymuszenie typu, jest to język typu kaczka. Ale nadal musisz sobie wyobrazić interfejs i musisz abstrakcyjnie zaprojektować swój umysł, aby poprawnie go wdrożyć.
Tak więc, choć twoje pytanie może brzmieć interesująco, oznacza to, że masz problemy ze zrozumieniem lub zastosowaniem technik wstrzykiwania zależności.
Aby bezpośrednio odpowiedzieć na twoje pytanie, Ruby i PHP mają świetną infrastrukturę próbną, zarówno wbudowaną w ramy testów jednostkowych, jak i dostarczaną osobno (patrz Kpina dla PHP). W niektórych przypadkach ramy te pozwalają nawet robić to, co sugerujesz, na przykład wyśmiewać wywołania statyczne lub inicjować obiekty bez jawnego wstrzykiwania zależności.
źródło