Jak rozpocząć korzystanie z TDD do kodowania prostych funkcji?

9

Zasadniczo mam sedno TDD. Sprzedano mi, że jest to przydatne i mam rozsądną znajomość frameworka MSTEST. Jednak do tej pory nie udało mi się ukończyć z wykorzystaniem go jako podstawowej metody rozwoju. Najczęściej używam go jako surogatu do pisania aplikacji konsolowych jako sterowników testowych (moje tradycyjne podejście).

Najbardziej użyteczną rzeczą dla mnie jest sposób, w jaki pochłania rolę testowania regresji.

Nie zbudowałem jeszcze niczego, co konkretnie izoluje różne zachowania, które można przetestować, co jest kolejną dużą częścią obrazu, który znam.

Tak więc pytanie polega na zapytaniu o wskazówki, jakie pierwsze testy mogę napisać dla następującego zadania programistycznego: Chcę stworzyć kod, który zawiera wykonanie zadania w stylu producenta / konsumenta.

Zatrzymałem się i postanowiłem napisać to pytanie po tym, jak napisałem ten kod (zastanawiam się, czy tym razem naprawdę mogę użyć TDD)

Kod:

interface ITask
{
    Guid TaskId { get; }
    bool IsComplete { get; }
    bool IsFailed { get; }
    bool IsRunning { get; }
}

interface ITaskContainer
{
    Guid AddTask(ICommand action);
}

interface ICommand
{
    string CommandName { get; }
    Dictionary<string, object> Parameters { get; }
    void Execute();
}
Aaron Anodide
źródło
Najpierw powinieneś napisać testy, a następnie interfejsy! Cały pomysł polega na tym, że TDD jest dla twojego API.
1
Jak pisać testy dla interfejsów, które jeszcze nie istnieją? Nie będą nawet kompilować.
Robert Harvey
5
To twój pierwszy nieudany test.
cori

Odpowiedzi:

10

Począwszy od tej koncepcji:
1) Zacznij od pożądanego zachowania. Napisz na to test. Zobacz test zakończony niepowodzeniem.
2) Napisz wystarczającą ilość kodu, aby pomyślnie przejść test. Zobacz wszystkie testy zaliczone.
3) Poszukaj kodu nadmiarowego / niechlujstwa -> refaktora. Zobacz, testy wciąż się kończą. Idź 1

Tak więc w punkcie 1 powiedzmy, że chcesz utworzyć nowe polecenie (rozciągam się na sposób działania polecenia, więc trzymaj się mnie). (Będę też bardziej pragmatyczny niż ekstremalny TDD)

Nowe polecenie nazywa się MakeMyLunch, więc najpierw tworzysz test, aby go utworzyć i uzyskać nazwę polecenia:

@Test
public void instantiateMakeMyLunch() {
   ICommand command = new MakeMyLunchCommand();
   assertEquals("makeMyLunch",command.getCommandName());
}

To się nie udaje, zmuszając cię do utworzenia nowej klasy poleceń i zwrócenia jej nazwy (purysta powiedziałby, że to dwie rundy TDD, a nie 1). Tworzysz klasę i implementujesz interfejs ICommand, w tym zwracając nazwę polecenia. Uruchomienie wszystkich testów pokazuje teraz wszystkie zaliczenia, więc zaczynasz szukać możliwości refaktoryzacji. Prawdopodobnie żaden.

Następnie chcesz, aby implementacja wykonała. Musisz więc zapytać: skąd mam wiedzieć, że „MakeMyLunch” z powodzeniem „zrobił mój lunch”. Jakie zmiany w systemie spowodowane tą operacją? Czy mogę to przetestować?

Załóżmy, że łatwo jest przetestować:

@Test
public void checkThatMakeMyLunchIsSuccessful() {
   ICommand command = new MakeMyLunchCommand();
   command.execute();
   assertTrue( Lunch.isReady() );
}

Innym razem jest to trudniejsze i naprawdę chcesz przetestować obowiązki badanego obiektu (MakeMyLunchCommand). Być może obowiązkiem MakeMyLunchCommand jest interakcja z lodówką i kuchenką mikrofalową. Aby to przetestować, możesz użyć fałszywej lodówki i fałszywej kuchenki mikrofalowej. [dwie przykładowe frameworki to Mockito i nMock lub spójrz tutaj .]

W takim przypadku zrobiłbyś coś takiego jak następujący pseudo kod:

@Test
public void checkThatMakeMyLunchIsSuccessful() {
   Fridge mockFridge = mock(Fridge);
   Microwave mockMicrowave = mock(Microwave);
   ICommand command = new MakeMyLunchCommand( mockFridge, mockMicrowave );
   command.execute();
   mockFramework.assertCalled( mockFridge.removeFood );
   mockFramework.assertCalled( microwave.turnon );
}

Purysta mówi, aby przetestować odpowiedzialność swojej klasy - jej interakcje z innymi klasami (czy polecenie otworzyło lodówkę i włączyło kuchenkę mikrofalową?).

Pragmatysta mówi: przetestuj grupę zajęć i sprawdź wynik (czy lunch jest gotowy?).

Znajdź odpowiednią równowagę, która działa dla twojego systemu.

(Uwaga: pamiętaj, że być może doszedłeś do struktury interfejsu zbyt wcześnie. Być może możesz to ewoluować, pisząc testy jednostkowe i implementacje, aw kroku # 3 „zauważysz” powszechną możliwość interfejsu).

jayraynet
źródło
gdybym nie napisał wcześniej swojego interfejsu, jakie pytanie doprowadziłoby do stworzenia metody Execute () - niektóre z moich początkowych prób TDD utknęły w martwym punkcie, gdy nie miałem „kroku” do stymulowania dodatkowej funkcjonalności - dostaję to uczucie ukrytego problemu z kurczakiem / jajkiem, które należy
odsunąć na bok
1
Dobre pytanie! Jeśli jedynym poleceniem, które wykonałeś, było „MakeMyLunchCommand”, to metoda mogła rozpocząć się od „.makeMyLunch ()”. Co by było w porządku. Następnie wprowadź kolejne polecenie („NapCommand.takeNap ()”). Nadal nie ma problemu z różnymi metodami. Następnie zaczynasz używać go w ekosystemie, co jest prawdopodobne, gdy zmuszony jesteś uogólnić interfejs ICommand. Ogólnie rzecz biorąc, często opóźniasz generalizację do ostatniej odpowiedzialnej chwili, ponieważ YAGNI [ en.wikipedia.org/wiki/You_ain't_gonna_need_it ] =) Innym razem wiesz, że tam dotrzesz, więc zacznij od tego.
jayraynet,
(Zakładamy również, że używasz nowoczesnego IDE, dzięki czemu refaktoryzacja rzeczy takich jak nazwy metod jest banalna)
jayraynet
1
jeszcze raz dziękuję za radę, to dla mnie kamień milowy, że w końcu widzę wszystkie elementy i to, jak pasują - i tak, szybki refaktor jest w moim
zestawie