Co zrobić, gdy testy TDD ujawniają nową potrzebną funkcjonalność, która również wymaga testów?

13

Co robisz, gdy piszesz test i dochodzisz do punktu, w którym musisz zdać test, i zdajesz sobie sprawę, że potrzebujesz dodatkowej funkcjonalności, która powinna być podzielona na własną funkcję? Ta nowa funkcja również musi zostać przetestowana, ale cykl TDD mówi: „Nie udać się testem, sprawić, by przeszedł, a następnie refaktoryzować”. Jeśli jestem na etapie, w którym próbuję zdać test, nie powinienem zaczynać i uruchamiać kolejnego testu zakończonego niepowodzeniem w celu przetestowania nowej funkcjonalności, którą muszę zaimplementować.

Na przykład piszę klasę punktową, która ma funkcję WillCollideWith ( LineSegment ) :

public class Point {
    // Point data and constructor ...

    public bool CollidesWithLine(LineSegment lineSegment) {
        Vector PointEndOfMovement = new Vector(Position.X + Velocity.X,
                                               Position.Y + Velocity.Y);
        LineSegment pointPath = new LineSegment(Position, PointEndOfMovement);
        if (lineSegment.Intersects(pointPath)) return true;
        return false;
    }
}

Pisałem test dla CollidesWithLine, kiedy zdałem sobie sprawę, że potrzebuję funkcji LineSegment.Intersects ( LineSegment ) . Ale czy powinienem po prostu przerwać to, co robię w cyklu testowym, aby stworzyć nową funkcjonalność? To wydaje się łamać zasadę „czerwony, zielony, refaktor”.

Czy powinienem po prostu napisać kod, który wykrywa, że ​​lineSegments przecinają się wewnątrz funkcji CollidesWithLine i refaktoryzują go po jego zadziałaniu ? To by działało w tym przypadku, ponieważ mogę uzyskać dostęp do danych z LineSegment , ale co z przypadkami, w których tego rodzaju dane są prywatne?

Joshua Harris
źródło

Odpowiedzi:

14

Po prostu skomentuj swój test i najnowszy kod (lub włóż go do schowka), aby w rzeczywistości cofnąć zegar do początku cyklu. Następnie zacznij od LineSegment.Intersects(LineSegment)testu / kodu / refaktora. Kiedy to zrobisz, odkomentuj swój poprzedni test / kod (lub wyciągnij ze skrytki) i trzymaj się cyklu.

Javier
źródło
Czym różni się to od ignorowania go i powrotu do niego później?
Joshua Harris
1
tylko drobne szczegóły: w raportach nie ma dodatkowego testu „zignoruj ​​mnie”, a jeśli używasz skrytek, kod jest nie do odróżnienia od „czystej” sprawy.
Javier,
Co to jest skrytka? czy to jest jak kontrola wersji?
Joshua Harris
1
niektóre VCS implementują go jako funkcję (przynajmniej Git i Fossil). Pozwala usunąć zmianę, ale zapisać ją do ponownego zastosowania później. Nie jest to trudne ręcznie, wystarczy zapisać różnicę i powrócić do ostatniego stanu. Później ponownie zastosuj różnicę i kontynuuj.
Javier,
6

W cyklu TDD:

W fazie „wykonaj test pozytywny” powinieneś napisać najprostszą implementację, która spowoduje pozytywny wynik testu . Aby pomyślnie przejść test, postanowiłeś utworzyć nowego współpracownika, który poradziłby sobie z brakującą logiką, ponieważ włożenie klasy punktowej do wykonania testu może być zbyt dużym nakładem pracy. Na tym polega problem. Podejrzewam, że test, który chcesz zaliczyć, był zbyt dużym krokiem . Myślę więc, że problem leży w samym teście, powinieneś usunąć / skomentować ten test i wymyślić prostszy test, który pozwoli ci zrobić mały krok bez wprowadzania LineSegment.Intersects (LineSegment). Po przejściu tego testu możesz następnie refaktoryzowaćtwój kod (tutaj zastosujesz zasadę SRP), przenosząc tę ​​nową logikę do metody LineSegment.Intersects (LineSegment). Twoje testy nadal będą zaliczane, ponieważ nie zmieniłeś żadnego zachowania, a jedynie przeniosłeś trochę kodu.

Na twoim obecnym rozwiązaniu projektowym

Ale dla mnie masz głębszy problem projektowy polegający na tym, że naruszasz zasadę pojedynczej odpowiedzialności . Rolą Punktu jest… być punktem, to wszystko. Bycie punktem nie ma mądrości, jest to wartość sprawiedliwa i xiy. Punkty są typami wartości . To samo dotyczy segmentów, segmenty są typami wartości złożonymi z dwóch punktów. Mogą zawierać odrobinę „inteligencji”, na przykład, aby obliczyć ich długość na podstawie ich pozycji punktowej. Ale to jest to.

Teraz decyzja, czy punkt i segment kolidują, jest sama w sobie odpowiedzialnością. Z pewnością jest to zbyt wiele pracy, aby sam punkt lub segment mógł sobie z tym poradzić. Nie może należeć do klasy Point, ponieważ w przeciwnym razie Punkty będą wiedzieć o segmentach. I nie może należeć do segmentów, ponieważ segmenty już są odpowiedzialne za dbanie o punkty w segmencie, a może także obliczanie długości samego segmentu.

Tak więc odpowiedzialność powinna spoczywać na innej klasie, takiej jak na przykład „PointSegmentCollisionDetector”, która miałaby metodę taką jak:

bool AreInCollision (punkt p, segmenty)

I to jest coś, co przetestowałbyś osobno w punktach i segmentach.

Zaletą tego projektu jest to, że teraz możesz mieć inną implementację detektora kolizji. Łatwo byłoby na przykład przetestować silnik gry (zakładam, że piszesz grę: p), zmieniając metodę wykrywania kolizji w czasie wykonywania. Lub wykonać wizualne sprawdzanie / eksperymenty w czasie wykonywania między różnymi strategiami wykrywania kolizji.

W tej chwili, umieszczając tę ​​logikę w swojej klasie punktowej, blokujesz rzeczy i przesuwasz zbyt dużą odpowiedzialność na klasę punktową.

Mam nadzieję, że to ma sens

kod kreskowy
źródło
Masz rację, że próbowałem przetestować zbyt dużą zmianę i myślę, że masz rację, dzieląc ją na klasę kolizyjną, ale to sprawia, że ​​zadaję zupełnie nowe pytanie, z którym mógłbyś mi pomóc: czy powinienem użyć interfejsu, gdy metody są tylko podobne? .
Joshua Harris
2

Najłatwiejszą rzeczą do zrobienia w stylu TDD byłoby wyodrębnienie interfejsu dla LineSegment i zmiana parametru metody w celu uwzględnienia interfejsu. Następnie możesz wyśmiewać segment linii wejściowej i niezależnie kodować / testować metodę przecinania.

Dan Lyons
źródło
1
Wiem, że jest to metoda TDD, którą najczęściej słyszę, ale ILineSegment nie ma sensu. Jedną rzeczą jest interfejs zewnętrzny zasobu lub coś, co może przybierać różne formy, ale nie widzę żadnego powodu, dla którego kiedykolwiek dołączałbym jakąkolwiek funkcjonalność do czegoś innego niż segment linii.
Joshua Harris
0

Dzięki jUnit4 możesz użyć @Ignoreadnotacji do testów, które chcesz odłożyć.

Dodaj adnotację do każdej metody, którą chcesz odłożyć, i kontynuuj pisanie testów pod kątem wymaganej funkcjonalności. Odwróć z powrotem, aby później przekształcić starsze przypadki testowe.

bakoyaro
źródło