Jak poprawić błąd w teście, po napisaniu implementacji


21

Jaki jest najlepszy sposób działania w TDD, jeśli po prawidłowym zaimplementowaniu logiki test nadal się nie powiedzie (ponieważ w teście jest błąd)?

Załóżmy na przykład, że chcesz rozwinąć następującą funkcję:

int add(int a, int b) {
    return a + b;
}

Załóżmy, że rozwijamy go w następujących krokach:

  1. Test zapisu (jeszcze brak funkcji):

    // test1
    Assert.assertEquals(5, add(2, 3));
    

    Powoduje błąd kompilacji.

  2. Napisz implementację funkcji fikcyjnej:

    int add(int a, int b) {
        return 5;
    }
    

    Wynik: test1zalicza się.

  3. Dodaj kolejny przypadek testowy:

    // test2 -- notice the wrong expected value (should be 11)!
    Assert.assertEquals(12, add(5, 6));
    

    Wynik: test2zawodzi, test1nadal mija.

  4. Napisz prawdziwą implementację:

    int add(int a, int b) {
        return a + b;
    }
    

    Wynik: test1nadal mija, test2nadal nie działa (od 11 != 12).

W tym konkretnym przypadku: czy lepiej byłoby:

  1. popraw test2i zobacz, że teraz mija, lub
  2. usuń nową część implementacji (tj. wróć do kroku 2 powyżej), popraw test2i pozwól, aby zakończyła się niepowodzeniem, a następnie ponownie wprowadź poprawną implementację (krok # 4 powyżej).

A może jest jakiś inny, mądrzejszy sposób?

Rozumiem, że przykładowy problem jest dość trywialny, ale interesuje mnie, co robić w ogólnym przypadku, który może być bardziej złożony niż dodanie dwóch liczb.

EDYCJA (W odpowiedzi na odpowiedź @Thomas Junk):

W centrum tego pytania jest to, co sugeruje TDD w takim przypadku, a nie „uniwersalna najlepsza praktyka” do uzyskania dobrego kodu lub testów (które mogą być inne niż sposób TDD).



5
Oczywiście musisz wykonywać TDD na swoim TDD.
Blrfl

17
Jeśli ktokolwiek zapyta mnie, dlaczego jestem sceptyczny wobec TDD, wskażę mu to pytanie. To jest Kafkaesque.
Traubenfuchs

@Blrfl tak mówi nam Xibit »Umieściłem TDD w TDD, abyś mógł TDD podczas TDDing«: D
Thomas Junk

3
@Traubenfuchs Przyznaję, że pytanie na pierwszy rzut oka wydaje się głupie i nie jestem zwolennikiem „robienia TDD przez cały czas”, ale wierzę, że jest duża korzyść z powodu niepowodzenia testu, a następnie napisać kod, który powoduje, że test się powiedzie (w końcu o to właściwie chodzi).
Vincent Savard

Odpowiedzi:


31

Absolutnie krytyczną rzeczą jest to, że widzisz test zarówno pozytywny jak i negatywny.

Nieważne, czy usuniesz kod, aby test się nie powiódł, a następnie przepisz kod lub przekradnij go do schowka, aby wkleić go później. TDD nigdy nie powiedział, że musisz coś przepisać. Chce wiedzieć, że test zalicza się tylko wtedy, gdy powinien przejść, a test kończy się niepowodzeniem tylko wtedy, gdy się nie powiedzie.

Widzenie testu, zarówno pozytywnego, jak i negatywnego, jest sposobem na przetestowanie testu. Nigdy nie ufaj testowi, którego nigdy nie widziałeś.


Refaktoryzacja w stosunku do czerwonego paska daje nam formalne kroki w celu refaktoryzacji testu roboczego:

  • Uruchom test
    • Zwróć uwagę na zielony pasek
    • Złam testowany kod
  • Uruchom test
    • Zwróć uwagę na czerwony pasek
    • Przeanalizuj test
  • Uruchom test
    • Zwróć uwagę na czerwony pasek
    • Odblokuj testowany kod
  • Uruchom test
    • Zwróć uwagę na zielony pasek

Jednak nie refaktoryzujemy testu roboczego. Musimy przekształcić test na błędy. Jednym z problemów jest kod, który został wprowadzony, a obejmował go tylko ten test. Taki kod powinien zostać wycofany i ponownie wprowadzony po naprawieniu testu.

Jeśli tak nie jest, a pokrycie kodu nie stanowi problemu z powodu innych testów obejmujących kod, możesz przekształcić test i wprowadzić go jako test zielony.

W tym przypadku kod jest również wycofywany, ale na tyle, aby spowodować niepowodzenie testu. Jeśli to nie wystarczy, aby pokryć cały wprowadzony kod, podczas gdy jest on objęty tylko błędnym testem, potrzebujemy większego wycofania kodu i kolejnych testów.

Wprowadź zielony test

  • Uruchom test
    • Zwróć uwagę na zielony pasek
    • Złam testowany kod
  • Uruchom test
    • Zwróć uwagę na czerwony pasek
    • Odblokuj testowany kod
  • Uruchom test
    • Zwróć uwagę na zielony pasek

Złamanie kodu może oznaczać komentarz lub przeniesienie go w inne miejsce, aby później wkleić go z powrotem. To pokazuje nam zakres kodu, który obejmuje test.

W przypadku tych dwóch ostatnich serii wracasz do normalnego cyklu czerwonego zielonego. Po prostu wklejasz zamiast pisać, aby odblokować kod i przejść test. Upewnij się więc, że wklejasz tylko tyle, aby test zdał pomyślnie.

Ogólny wzór polega na tym, że kolor testu zmienia się w oczekiwany sposób. Pamiętaj, że powoduje to sytuację, w której masz krótkotrwały niezaufany test ekologiczny. Uważaj, aby nie przeszkadzać ci i zapominać, gdzie jesteś na tych etapach.

Moje podziękowania dla RubberDuck za link Embracing the Red Bar .


2
Najbardziej podoba mi się ta odpowiedź: ważne jest, aby zobaczyć, że test kończy się niepowodzeniem z niepoprawnym kodem, więc usunęłbym / skomentował kod, poprawił testy i zobaczył, że zawiodły, odłożył kod (może wprowadzić celowy błąd, aby testy były test) i popraw kod, aby działał. To bardzo XP, aby go całkowicie usunąć i przepisać, ale czasami musisz być po prostu pragmatyczny. ;)
GolezTrol

@GolezTrol Myślę, że moja odpowiedź mówi to samo, dlatego doceniłbym wszelkie opinie na temat niejasności.
jonrsharpe

@jonrsharpe Twoja odpowiedź też jest dobra i głosowałem za nią, zanim jeszcze ją przeczytałem. Ale w przypadku bardzo surowego przywracania kodu CandiedOrange sugeruje bardziej pragmatyczne podejście, które bardziej do mnie przemawia.
GolezTrol

@GolezTrol Nie powiedziałem, jak przywrócić kod; skomentuj, wytnij i wklej, schowaj, użyj historii swojego IDE; to naprawdę nie ma znaczenia. Najważniejsze jest to, dlaczego to robisz: abyś mógł sprawdzić, czy masz właściwą awarię. Mam nadzieję, że zredagowałem.
jonrsharpe

10

Jaki jest ogólny cel , który chcesz osiągnąć?

  • Robisz niezłe testy?

  • Dokonywania prawidłowej realizacji?

  • Czy TTD ma rację religijną ?

  • Żadne z powyższych?

Być może przerastasz swój związek z testami i testowaniem.

Testy nie gwarantują poprawności wdrożenia. Po zaliczeniu wszystkich testów nic nie mówi o tym, czy oprogramowanie robi to, co powinno; nie czyni esencjalizmu wypowiedzi na temat twojego oprogramowania.

Biorąc twój przykład:

„Prawidłowa” implementacja tego dodatku byłaby kodem równoważnym do a+b. I dopóki twój kod to robi , powiedziałbyś, że algorytm jest poprawny w tym, co robi i jest poprawnie zaimplementowany.

int add(int a, int b) {
    return a + b;
}

Na pierwszy rzut oka oboje zgodzilibyśmy się, że jest to wdrożenie dodatku.

Ale tak naprawdę nie mówimy, że ten kod jest jego implementacją addition, zachowuje się tylko w pewnym stopniu : pomyśl o przepełnieniu liczb całkowitych .

Przepełnienie liczb całkowitych ma miejsce w kodzie, ale nie w koncepcji addition. Tak więc: Twój kod zachowuje się w pewnym stopniu jak koncepcja addition, ale tak nie jest addition.

Ten raczej filozoficzny punkt widzenia ma kilka konsekwencji.

Jednym z nich jest to, że można powiedzieć, że testy są jedynie założeniami oczekiwanego zachowania twojego kodu. Testując swój kod, możesz (być może) nigdy nie upewnić się, że Twoja implementacja jest poprawna , najlepiej powiedzieć, że twoje oczekiwania dotyczące wyników dostarczonych przez kod były lub nie zostały spełnione; niech tak będzie, że twój kod jest zły, niech tak będzie, że twój test jest zły, albo niech będzie, że oba są w błędzie.

Przydatne testy pomagają ustalić Twoje oczekiwania co do tego, co powinien zrobić kod: tak długo, jak nie zmieniam swoich oczekiwań i dopóki zmodyfikowany kod daje mi oczekiwany wynik, mogę być pewien, że założenia, które podjąłem wyniki wydają się działać.

To nie pomaga, gdy dokonałeś błędnych założeń; ale hej! przynajmniej zapobiega schizofrenii: oczekiwanie różnych rezultatów, gdy nie powinno ich być.


tl; dr

Jaki jest najlepszy sposób działania w TDD, jeśli po prawidłowym zaimplementowaniu logiki test nadal się nie powiedzie (ponieważ w teście jest błąd)?

Twoje testy są założeniami dotyczącymi zachowania kodu. Jeśli masz dobry powód, by sądzić, że Twoja implementacja jest poprawna, napraw test i sprawdź, czy to założenie się spełni.


1
Myślę, że pytanie o ogólne cele jest dość ważne, dziękuję za jego poruszenie. Dla mnie najwyższe prio to: 1. poprawna implementacja 2. „ładne” testy (lub raczej „przydatne” / „dobrze zaprojektowane” testy). Widzę TDD jako możliwe narzędzie do osiągnięcia tych dwóch celów. Tak więc, chociaż niekoniecznie chcę religijnie podążać za TDD, w kontekście tego pytania najbardziej interesuje mnie perspektywa TDD. Przeredaguję pytanie, aby to wyjaśnić.
Attilio

Czy napisałbyś test, który testuje przepełnienie i przechodzi, gdy tak się dzieje, czy też powodowałby niepowodzenie, gdy tak się dzieje, ponieważ algorytm jest dodawany, a przepełnienie daje złą odpowiedź?
Jerry Jeremiah

1
@JerryJeremiah Chodzi mi o to: To, co powinny obejmować twoje testy, zależy od twojego przypadku użycia. W przypadku użycia, w którym dodajesz kilka pojedynczych cyfr, algorytm jest wystarczająco dobry . Jeśli wiesz, że bardzo prawdopodobne jest, że dodasz „duże liczby”, datatypejest to zdecydowanie zły wybór. Test wykazałby, że: Twoje oczekiwania byłyby „skuteczne dla dużych liczb” i w niektórych przypadkach nie zostały spełnione. Wtedy pytanie brzmiałoby, jak sobie poradzić z tymi sprawami. Czy to są narożne skrzynki? Kiedy tak, jak sobie z nimi poradzić? Być może niektóre klauzule quard pomagają zapobiec większemu bałaganowi. Odpowiedź jest związana z kontekstem.
Thomas Junk

7

Musisz wiedzieć, że test się nie powiedzie, jeśli implementacja jest niepoprawna, co nie jest równoznaczne z zaliczeniem, jeśli implementacja jest poprawna. Dlatego powinieneś przywrócić kod do stanu, w którym spodziewasz się, że zawiedzie przed poprawieniem testu, i upewnij się, że zawodzi z powodu, którego się spodziewałeś (tj. 5 != 12), Zamiast czegoś innego, czego nie przewidziałeś.


Jak możemy sprawdzić, czy test kończy się niepowodzeniem z oczekiwanego powodu?
Basilevs,

2
@Basilevs you: 1. postawić hipotezę, jaka powinna być przyczyna niepowodzenia; 2. uruchom test; oraz 3. przeczytaj wynikowy komunikat o błędzie i porównaj. Czasami sugeruje to również sposoby przepisania testu, aby uzyskać bardziej znaczący błąd (na przykład assertTrue(5 == add(2, 3))daje mniej użyteczne dane wyjściowe niż assertEqual(5, add(2, 3))pomimo tego, że oba testują to samo).
jonrsharpe

Nadal nie jest jasne, jak zastosować tę zasadę tutaj. Mam hipotezę - test zwraca stałą wartość, w jaki sposób ponowne uruchomienie tego samego testu zapewniłoby mi rację? Oczywiście, aby to sprawdzić, potrzebuję INNEGO testu. Sugeruję dodać wyraźny przykład, aby odpowiedzieć.
Basilevs,

1
@Basilevs co? Twoja hipoteza tutaj w kroku 3 brzmiałaby: „test kończy się niepowodzeniem, ponieważ 5 nie jest równe 12” . Uruchomienie testu pokaże, czy test nie powiedzie się z tego powodu, w takim przypadku kontynuujesz, czy z jakiegoś innego powodu, w którym to przypadku wiesz, dlaczego. Być może jest to problem językowy, ale nie jest dla mnie jasne, co sugerujesz.
jonrsharpe

5

W tym konkretnym przypadku, jeśli zmienisz 12 na 11, a test się teraz powiedzie, myślę, że dobrze wykonałeś testowanie testu, a także jego implementacji, więc nie ma potrzeby przechodzenia przez dodatkowe obręcze.

Ten sam problem może jednak pojawić się w bardziej złożonych sytuacjach, na przykład w przypadku błędu w kodzie instalacyjnym. W takim przypadku po naprawieniu testu prawdopodobnie powinieneś spróbować zmutować implementację w taki sposób, aby ten konkretny test się nie powiódł, a następnie cofnąć mutację. Jeśli cofnięcie implementacji jest najłatwiejszym sposobem, to jest w porządku. W twoim przykładzie możesz mutowaća + b do a + alub a * b.

Alternatywnie, jeśli możesz nieco mutować twierdzenie i zobaczyć, że test się nie powiedzie, może to być dość skuteczne w testowaniu testu.


0

Powiedziałbym, że jest to przypadek twojego ulubionego systemu kontroli wersji:

  1. Dokonaj korekty testu, utrzymując zmiany kodu w katalogu roboczym.
    Zatwierdź z odpowiednią wiadomością Fixed test ... to expect correct output.

    W przypadku gitmoże to wymagać użycia opcji, git add -pjeśli test i implementacja znajdują się w tym samym pliku, w przeciwnym razie można oczywiście po prostu rozdzielić oba pliki osobno.

  2. Zatwierdź kod implementacyjny.

  3. Cofnij się w czasie, aby przetestować zatwierdzenie dokonane w kroku 1, upewniając się, że test faktycznie się nie powiedzie .

Widzisz, w ten sposób nie polegasz na swojej umiejętności edytowania, aby przenieść kod implementacji z drogi podczas testowania testu zakończonego niepowodzeniem. Zatrudniasz swój VCS, aby zapisać swoją pracę i upewnić się, że zapisana historia VCS poprawnie obejmuje zarówno test negatywny, jak i pozytywny.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.