W TDD, jeśli napiszę przypadek testowy, który przejdzie bez modyfikacji kodu produkcyjnego, co to oznacza?

17

Oto zasady Roberta C. Martina dla TDD :

  • Nie wolno pisać żadnego kodu produkcyjnego, chyba że ma to negatywny wynik pozytywnego testu jednostkowego.
  • Nie wolno pisać więcej testów jednostkowych niż jest to wystarczające do zaliczenia; awarie kompilacji to awarie.
  • Nie wolno pisać więcej kodu produkcyjnego, niż jest to wystarczające do zaliczenia jednego z nieudanych testów jednostkowych.

Kiedy piszę test, który wydaje się opłacalny, ale przechodzi bez zmiany kodu produkcyjnego:

  1. Czy to znaczy, że zrobiłem coś złego?
  2. Czy powinienem unikać pisania takich testów w przyszłości, jeśli można im pomóc?
  3. Czy powinienem tam zostawić test, czy go usunąć?

Uwaga: Ja próbuje zadać to pytanie tutaj: Mogę zacząć od testów jednostkowych przechodzącej? Ale do tej pory nie byłem w stanie wystarczająco dobrze sformułować pytania.

Daniel Kaplan
źródło
„Kata Gra w kręgle”, do którego odwołuje się cytowany artykuł, ma w tej chwili ostatni test.
jscs,

Odpowiedzi:

21

Mówi, że nie można napisać kodu produkcyjnego, chyba że ma on przejść test jednostkowy zakończony niepowodzeniem, nie że nie można napisać testu, który przejdzie od samego początku. Reguła ma na celu powiedzenie „Jeśli musisz edytować kod produkcyjny, upewnij się, że najpierw napisałeś lub zmieniłeś test”.

Czasami piszemy testy, aby udowodnić teorię. Test kończy się pomyślnie, co obala naszą teorię. Następnie nie usuwamy testu. Możemy jednak (wiedząc, że mamy kontrolę źródłową) przerwać kod produkcyjny, aby upewnić się, że rozumiemy, dlaczego on przeszedł, gdy się tego nie spodziewaliśmy.

Jeśli okaże się, że jest to poprawny i poprawny test i nie powiela on istniejącego testu, zostaw go tam.

pdr
źródło
Poprawa zasięgu testowego istniejącego kodu jest kolejnym całkowicie poprawnym powodem do napisania (mam nadzieję) pozytywnego wyniku testu.
Jack
13

Oznacza to, że albo:

  1. Napisałeś kod produkcyjny, który spełnia pożądaną funkcję, nie pisząc najpierw testu (naruszenie „religijnego TDD”), lub
  2. Funkcja, której potrzebujesz, jest już spełniona przez kod produkcyjny, a piszesz tylko kolejny test jednostkowy, aby objąć tę funkcję.

Ta ostatnia sytuacja jest bardziej powszechna, niż mogłoby się wydawać. Jako całkowicie podstępny i trywialny (ale wciąż ilustrujący) przykład, powiedzmy, że napisałeś następujący test jednostkowy (pseudokod, bo jestem leniwy):

public void TestAddMethod()
{
    Assert.IsTrue(Add(2,3) == 5);
}

Ponieważ wszystko, czego naprawdę potrzebujesz, to wynik 2 i 3 dodanych razem.

Twoja metoda wdrożenia to:

public int add(int x, int y)
{
    return x + y;
}

Ale powiedzmy, że muszę teraz dodać 4 i 6 razem:

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

Nie muszę przepisywać mojej metody, ponieważ obejmuje ona już drugi przypadek.

Powiedzmy teraz, że dowiedziałem się, że moja funkcja Add naprawdę musi zwrócić liczbę o pewnym pułapie, powiedzmy 100. Mogę napisać nową metodę, która to przetestuje:

public void TestAddMethod3()
{
    Assert.IsTrue(Add(100,100) == 100);
}

I ten test się teraz nie powiedzie. Teraz muszę przepisać moją funkcję

public int add(int x, int y)
{
    var a = x + y;
    return a > 100 ? 100 : a;
}

aby przejść.

Zdrowy rozsądek nakazuje, że jeśli

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

przechodzi, celowo nie powoduje to niepowodzenia metody, aby można było przejść test zakończony niepowodzeniem, aby można było napisać nowy kod umożliwiający zaliczenie testu.

Robert Harvey
źródło
5
Jeśli w pełni poszedłeś za przykładami Martina (a on niekoniecznie to sugeruje), aby add(2,3)przejść, dosłownie zwróciłbyś 5. Zakodowane. Następnie napisałbyś test, dla add(4,6)którego zmusiłbyś cię do napisania kodu produkcyjnego, który sprawia, że ​​przechodzi on, nie przerywając add(2,3)jednocześnie. Można by skończyć z return x + y, ale nie ruszy się z nim. W teorii. Oczywiście Martin (a może to ktoś inny, nie pamiętam) lubi dawać takie przykłady edukacji, ale nie spodziewa się, że w ten sposób napiszesz tak trywialny kod.
Anthony Pegram
1
@tieTYT, ogólnie, jeśli dobrze pamiętam z książki Martina, drugi przypadek testowy zwykle wystarcza, aby napisać ogólne rozwiązanie dla prostej metody (a tak naprawdę po prostu sprawiłbyś, aby działał pierwszy raz). Nie potrzeba trzeciej.
Anthony Pegram
2
@tieTYT, wtedy będziesz pisał testy, aż to zrobisz. :)
Anthony Pegram
4
Istnieje trzecia możliwość i jest to sprzeczne z twoim przykładem: napisałeś duplikat testu. Jeśli postępujesz zgodnie z TDD „religijnie”, to nowy test, który przechodzi, jest zawsze zawsze czerwoną flagą. Po DRY nigdy nie powinieneś pisać dwóch testów, które testują zasadniczo to samo.
congusbongus
1
„Jeśli w pełni zastosowałeś się do przykładów Martina (a on niekoniecznie to sugeruje), aby przekazać add (2,3), dosłownie zwróciłbyś 5. Zakodowane na stałe”. - to jest trochę ścisłe TDD, które zawsze mi się podobało, pomysł, że piszesz kod, o którym wiesz, że jest błędny w oczekiwaniu na przyszły test i udowodnienie tego. Co się stanie, jeśli ten przyszły test z jakiegoś powodu nigdy nie zostanie napisany, a koledzy zakładają, że „wszystkie testy - zielony” implikuje „cały kod poprawny”?
Julia Hayward
2

Twój test zdał, ale się nie mylisz. Myślę, że tak się stało, ponieważ kod produkcyjny od początku nie jest TDD.

Załóżmy, że kanoniczny (?) TDD. Nie ma kodu produkcyjnego, ale kilka przypadków testowych (to oczywiście zawsze kończy się niepowodzeniem). Dodajemy kod produkcyjny do przejścia. Następnie zatrzymaj się tutaj, aby dodać więcej przypadków testowych zakończonych niepowodzeniem. Ponownie dodaj kod produkcyjny do przekazania.

Innymi słowy, twój test może być rodzajem testu funkcjonalności, a nie prostym testem jednostki TDD. Są to zawsze cenne atuty dla jakości produktu.

Osobiście nie lubię takich totalitarnych, nieludzkich zasad; (

9dan
źródło
2

W rzeczywistości ten sam problem pojawił się na dojo zeszłej nocy.

Zrobiłem szybkie badania nad tym. Oto co wymyśliłem:

Zasadniczo nie jest to wyraźnie zabronione przez reguły TDD. Być może potrzebne są dodatkowe testy, aby udowodnić, że funkcja działa poprawnie w przypadku danych ogólnych. W tym przypadku praktyka TDD zostaje odłożona na chwilę. Zauważ, że wkrótce opuszczenie praktyki TDD niekoniecznie oznacza łamanie reguł TDD, o ile w międzyczasie nie zostanie dodany kod produkcyjny.

Dodatkowe testy mogą być napisane, o ile nie są zbędne. Dobrą praktyką byłoby wykonanie testów podziału klas równoważności. Oznacza to, że testowane są przypadki brzegowe i co najmniej jedna sprawa wewnętrzna dla każdej klasy równoważności.

Jednym z problemów, które mogą wystąpić przy takim podejściu, jest to, że jeśli testy przejdą od początku, nie można zagwarantować, że nie ma fałszywych wyników pozytywnych. Oznacza to, że mogą zdać testy, ponieważ testy nie zostaną poprawnie zaimplementowane, a nie dlatego, że kod produkcyjny działa poprawnie. Aby temu zapobiec, kod produkcyjny należy nieznacznie zmienić, aby przerwać test. Jeśli to spowoduje, że test się nie powiedzie, najprawdopodobniej test zostanie poprawnie zaimplementowany, a kod produkcyjny można zmienić z powrotem, aby test powiódł się ponownie.

Jeśli chcesz po prostu ćwiczyć ścisłe TDD, możesz nie pisać żadnych dodatkowych testów, które przejdą od początku. Z drugiej strony w środowisku programowania korporacyjnego należy zrezygnować z praktyki TDD, jeśli dodatkowe testy wydają się przydatne.

leifbattermann
źródło
0

Test, który przechodzi bez modyfikacji kodu produkcyjnego, nie jest z natury zły i często jest konieczny do opisania dodatkowego wymagania lub przypadku granicznego. Tak długo, jak twój test „wydaje się opłacalny”, tak jak mówisz, że tak, zachowaj go.

Problemem jest pisanie pozytywnego testu jako zamiennika faktycznego zrozumienia przestrzeni problemów.

Możemy sobie wyobrazić dwie skrajności: jeden programista, który pisze dużą liczbę testów „na wszelki wypadek”, jeden łapie błąd; oraz drugi programista, który dokładnie analizuje przestrzeń problemów przed napisaniem minimalnej liczby testów. Powiedzmy, że oboje próbują zaimplementować funkcję wartości bezwzględnej.

Pierwszy programista pisze:

assert abs(-88888) == 88888
assert abs(-12345) == 12345
assert abs(-5000) == 5000
assert abs(-32) == 32
assert abs(46) == 46
assert abs(50) == 50
assert abs(5001) == 5001
assert abs(999999) == 999999
...

Drugi programista pisze:

assert abs(-1) == 1
assert abs(0) == 0
assert abs(1) == 1

Implementacja pierwszego programisty może spowodować:

def abs(n):
    if n < 0:
        return -n
    elif n > 0:
        return n

Implementacja drugiego programisty może spowodować:

def abs(n):
    if n < 0:
        return -n
    else:
        return n

Wszystkie testy przeszły pomyślnie, ale pierwszy programista nie tylko napisał kilka redundantnych testów (niepotrzebnie spowalniając ich cykl rozwojowy), ale także nie przetestował przypadku granicznego ( abs(0)).

Jeśli zauważysz, że piszesz testy, które pomyślnie przejdziesz bez modyfikowania kodu produkcyjnego, zadaj sobie pytanie, czy testy naprawdę dodają wartości, czy też musisz poświęcić więcej czasu na zrozumienie przestrzeni problemów.

myślnictwo
źródło
Cóż, drugi programista był wyraźnie nieostrożny również w testach, ponieważ jego współpracownik przedefiniował abs(n) = n*ni zdał.
Eiko,
@Eiko Masz absolutną rację. Pisanie zbyt małej liczby testów może cię równie mocno ugryźć. Drugi programista był zbyt skąpy, przynajmniej nie testując abs(-2). Podobnie jak w przypadku wszystkiego, kluczem jest umiar.
thinkterry