Jak wyglądałby nowy język, gdyby został zaprojektowany od podstaw, aby był łatwy w TDD?

9

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?

Geoff
źródło
Co powiesz na język, który nie wymaga TDD? blog.8thlight.com/uncle-bob/2011/10/20/Simple-Hickey.html
Job
2
Żaden język nie wymaga TDD. TDD jest przydatną praktyką , a jednym z punktów Hickeya jest to, że sam test nie oznacza, że ​​możesz przestać myśleć .
Frank Shearar,
Test Driven Development polega na uzyskaniu prawidłowego wewnętrznego i zewnętrznego interfejsu API i rób to z góry. Stąd w Javie to jest wszystko na temat interfejsów - rzeczywiste klasy są produkty uboczne.

Odpowiedzi:

6

Wiele lat temu rzuciłem prototyp, który dotyczył podobnego pytania; oto zrzut ekranu:

Testowanie przycisku zerowego

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.

Carl Manaster
źródło
2
Haha, to niesamowite! Właściwie podoba mi się pomysł łączenia testów z kodem. Jest to dość żmudne (choć istnieją bardzo dobre powody) w .NET posiadającym osobne zestawy z równoległymi przestrzeniami nazw dla testów jednostkowych. Ułatwia także refaktoryzację, ponieważ przenoszenie kodu powoduje automatyczne przenoszenie testów: P
Geoff
Ale czy chcesz tam zostawić testy? Czy pozostawiłbyś je włączone dla kodu produkcyjnego? Może mogą być # jeśli zdefiniowane dla C, w przeciwnym razie sprawdzimy trafienia w rozmiar kodu / w czasie wykonywania.
Mawg mówi o przywróceniu Moniki
To wyłącznie prototyp. Gdyby to się stało, musielibyśmy wziąć pod uwagę takie parametry, jak wydajność i rozmiar, ale jest o wiele za wcześnie, aby się tym martwić, a jeśli kiedykolwiek do tego dojdziemy, nie będzie trudno wybrać, co pominąć lub, w razie potrzeby, pozostawić twierdzenia poza skompilowanym kodem. Dzięki za zainteresowanie.
Carl Manaster
5

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:

class MessageSender
  def send
    # Do something with a side effect
  end
end

Powiedzmy, że mamy MessageSenderUser, który korzysta z instancji MessageSender:

class MessageSenderUser

  def initialize(message_sender)
    @message_sender = message_sender
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

Zwróć uwagę na zastosowanie zastrzyku zależności , podstawowego zestawu testów jednostkowych. Wrócimy do tego.

Chcesz sprawdzić, czy MessageSenderUser#do_stuffpołą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:

class MockMessageSender

  attr_accessor :send_count

  def initialize
    @send_count = 0
  end

  def send
    @send_count += 1
  end

end

I użyj go w swoim teście:

mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)

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.

class MessageSenderUser

  def initialize
    @message_sender = MessageSender.new
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

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:

mock_message_sender = mock MessageSender
MessageSender.should_receive(:new).and_return(mock_message_sender)
mock_message_sender.should_receive(:send).twice.with(no_arguments)
MessageSenderUser.new.do_stuff

To cały test. Jeśli MessageSenderUser#do_stuffnie wywoła MessageSender#senddokł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.

Wayne Conrad
źródło
Świetna odpowiedź! Zaczynacie mi mówić z powrotem do języków dynamicznych :) Myślę, że kluczem jest tutaj pisanie kaczką, sztuczka z .new byłaby również możliwa w języku statycznym (choć byłby o wiele mniej elegancki).
Geoff,
3

Jak więc wyglądałby język, gdyby został zaprojektowany od zera, aby był świetny dla TDD?

„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.

Caleb
źródło
Zgadzam się, że bardzo trudno jest dobrze sformułować pytanie. Myślę, że mam na myśli to, że obecne narzędzia do testowania dla języków takich jak Java / C # wydają się przeszkadzać, a niektóre dodatkowe / alternatywne funkcje językowe uczyniłyby całość bardziej elegancką (tj. Brak interfejsów dla 90 % moich klas, tylko te, w których ma to sens z punktu widzenia projektowania wyższego poziomu).
Geoff,
0

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.

Patkos Csaba
źródło
1
Zgadzam się, że interfejsy są świetne i stanowią kluczowy element projektu. Jednak w moim kodzie stwierdzam, że 90% klas ma interfejs i że istnieją tylko dwie implementacje tego interfejsu, klasa i makiety tej klasy. Chociaż technicznie jest to dokładnie punkt interfejsów, nie mogę nie poczuć, że jest nieelegancki.
Geoff,
Nie znam się na wyśmiewaniu w Javie i C #, ale o ile wiem, wyśmiewany obiekt naśladuje prawdziwy obiekt. Często robię wstrzykiwanie zależności, używając parametru typu obiektu i zamiast tego wysyłając próbkę do metody / klasy. Coś w rodzaju funkcji someName (AnotherClass $ object = null) {$ this-> anotherObject = $ object? : nowy AnotherClass; } Jest to często stosowana sztuczka polegająca na wstrzykiwaniu zależności bez korzystania z interfejsu.
Patkos Csaba,
1
To jest zdecydowanie miejsce, w którym dynamiczne języki mają przewagę nad językami typu Java / C # w odniesieniu do mojego pytania. Typowa próbka konkretnej klasy stworzy podklasę klasy, co oznacza, że ​​zostanie wywołany konstruktor konkretnej klasy, co zdecydowanie należy unikać (są wyjątki, ale mają one swoje własne problemy). Dynamiczna próbka wykorzystuje tylko pisanie kaczek, więc nie ma związku między konkretną klasą a jej próbą. Często pisałem w Pythonie, ale to było przed moimi dniami TDD, być może nadszedł czas, aby spojrzeć jeszcze raz!
Geoff,