Jak robiąc TDD i pisząc test jednostkowy, jak oprzeć się pokusie „oszukiwania” podczas pisania pierwszej iteracji testowanego kodu „implementacyjnego”?
Na przykład:
muszę obliczyć silnię liczby. Zaczynam od testu jednostkowego (przy użyciu MSTest) czegoś takiego jak:
[TestClass]
public class CalculateFactorialTests
{
[TestMethod]
public void CalculateFactorial_5_input_returns_120()
{
// Arrange
var myMath = new MyMath();
// Act
long output = myMath.CalculateFactorial(5);
// Assert
Assert.AreEqual(120, output);
}
}
Uruchamiam ten kod i kończy się niepowodzeniem, ponieważ CalculateFactorial
metoda nawet nie istnieje. Więc teraz piszę pierwszą iterację kodu, aby zaimplementować testowaną metodę, pisząc minimalny kod wymagany do zaliczenia testu.
Chodzi o to, że ciągle mam ochotę napisać:
public class MyMath
{
public long CalculateFactorial(long input)
{
return 120;
}
}
Jest to technicznie poprawne, ponieważ tak naprawdę jest to minimalny kod wymagany do tego, aby ten konkretny test zdał (przejść na zielono), chociaż jest to oczywiście „oszustwo”, ponieważ tak naprawdę nie próbuje nawet wykonać funkcji obliczania silni. Oczywiście teraz część refaktoryzacji staje się ćwiczeniem w „pisaniu poprawnej funkcjonalności”, a nie prawdziwym refaktoryzowaniem implementacji. Oczywiście dodanie dodatkowych testów z różnymi parametrami zakończy się niepowodzeniem i wymusi refaktoryzację, ale musisz zacząć od tego jednego testu.
Moje pytanie brzmi więc, jak uzyskać równowagę między „pisaniem minimalnego kodu, aby przejść test”, a jednocześnie utrzymywać go w funkcjonowaniu i zgodnie z duchem tego, co faktycznie próbujesz osiągnąć?
źródło
Odpowiedzi:
To całkowicie legalne. Czerwony, zielony, refaktor.
Pierwszy test zalicza się.
Dodaj drugi test z nowym wejściem.
Teraz szybko przejdź do zielonego, możesz dodać if-else, który działa dobrze. To mija, ale jeszcze nie skończyłeś.
Najważniejsza jest trzecia część Red, Green, Refactor. Refaktoryzuj, aby usunąć duplikację . W twoim kodzie będzie teraz duplikat. Dwie instrukcje zwracające liczby całkowite. Jedynym sposobem na usunięcie tego powielania jest prawidłowe kodowanie funkcji.
Nie mówię, że nie pisz tego poprawnie za pierwszym razem. Mówię tylko, że to nie oszustwo, jeśli nie.
źródło
Oczywiście konieczne jest zrozumienie ostatecznego celu i osiągnięcie algorytmu, który spełnia ten cel.
TDD nie jest magiczną kulą do projektowania; nadal musisz wiedzieć, jak rozwiązywać problemy za pomocą kodu, i nadal musisz wiedzieć, jak to zrobić na poziomie wyższym niż kilka wierszy kodu, aby pomyślnie przejść test.
Podoba mi się pomysł TDD, ponieważ zachęca do dobrego projektowania; sprawia, że myślisz o tym, jak napisać kod, aby był on testowalny, i ogólnie rzecz biorąc, filozofia popchnie kod w kierunku lepszego ogólnego projektu. Ale nadal musisz wiedzieć, jak zaprojektować rozwiązanie.
Nie popieram redukcjonistycznych filozofii TDD, które twierdzą, że możesz rozwinąć aplikację, pisząc najmniejszą ilość kodu, aby przejść test. Bez myślenia o architekturze to nie zadziała, a twój przykład to potwierdza.
Wujek Bob Martin mówi:
źródło
Bardzo dobre pytanie ... i muszę się nie zgodzić z prawie wszystkimi oprócz @Robert.
Pisanie
wykonanie funkcji czynnikowej przez wykonanie jednego testu jest stratą czasu . To nie jest „oszukiwanie”, ani dosłownie podążanie za czerwono-zielonym refaktorem. Jest źle .
Dlatego:
argumenty „refaktoryzatora” są błędne; jeśli masz dwa przypadki testowe dla 5 i 6, kod ten jest wciąż złe, ponieważ nie jesteś obliczania silni w ogóle :
jeśli dosłownie podążymy za argumentem „refaktoryzator” , wówczas gdy będziemy mieli 5 przypadków testowych, wywołamy YAGNI i zaimplementujemy funkcję za pomocą tabeli odnośników:
Żaden z nich są rzeczywiście obliczenia niczego, jesteś . I to nie jest zadanie!
źródło
Kiedy napisałeś tylko jeden test jednostkowy, implementacja jednowierszowa (
return 120;
) jest uzasadniona. Pisanie pętli obliczającej wartość 120 - to byłoby oszustwo!Takie proste testy początkowe są dobrym sposobem na wyłapanie przypadków skrajnych i zapobieganie błędom jednorazowym. Pięć w rzeczywistości nie jest wartością wejściową, od której zacznę.
Przydatna tutaj może być zasada: zero, jeden, wiele, wiele . Zero i jeden są ważnymi przypadkami krawędzi dla silni. Mogą być realizowane za pomocą jednowarstwowych. Przypadek testowy „wiele” (np. 5!) Zmusiłby cię do napisania pętli. Przypadek testowy „partii” (1000 !?) może zmusić Cię do wdrożenia alternatywnego algorytmu do obsługi bardzo dużych liczb.
źródło
factorial(5)
jest to zły pierwszy test. zaczynamy od najprostszych możliwych przypadków i w każdej iteracji robimy testy bardziej szczegółowe, zachęcając kod, aby stał się nieco bardziej ogólny. to właśnie wujek Bob nazywa priorytetowym założeniem transformacji ( blog.8thlight.com/uncle-bob/2013/05/27/… )Tak długo, jak masz tylko jeden test, minimalny kod wymagany do zaliczenia testu jest naprawdę
return 120;
, i możesz go łatwo zachować, dopóki nie masz więcej testów.Umożliwia to odłożenie dalszego projektowania do momentu napisania testów, które wykonują INNE zwracane wartości tej metody.
Proszę pamiętać, że test jest runnable wersja specyfikacji, a jeśli wszystko że specyfikacja mówi, że f (6) = 120 następnie, że idealnie pasuje do tej roli.
źródło
Jeśli jesteś w stanie oszukiwać w taki sposób, sugeruje to, że twoje testy jednostkowe są wadliwe.
Zamiast testować metodę czynnikową z pojedynczą wartością, sprawdź, że był to zakres wartości. Testowanie oparte na danych może tutaj pomóc.
Zobacz swoje testy jednostkowe jako przejaw wymagań - muszą one wspólnie określać zachowanie metody, którą testują. (Jest to znane jako rozwój oparty na zachowaniu - jego przyszłość
;-)
)Więc zadaj sobie pytanie - gdyby ktoś zmienił implementację na coś niepoprawnego, czy Twoje testy nadal by się zdały, czy powiedzieliby „poczekaj chwilę!”?
Mając to na uwadze, jeśli twój jedyny test był tym, który dotyczy pytania, to technicznie odpowiednia implementacja jest poprawna. Problem jest następnie postrzegany jako źle zdefiniowane wymagania.
źródło
case
instrukcji doswitch
i nie możesz napisać testu dla każdego możliwego wejścia i wyjścia dla przykładu OP.Int64.MinValue
doInt64.MaxValue
. Uruchomienie zajęłoby dużo czasu, ale wyraźnie zdefiniowałoby to wymaganie bez miejsca na błędy. Przy obecnej technologii jest to niewykonalne (podejrzewam, że może stać się bardziej powszechne w przyszłości) i zgadzam się, że można oszukiwać, ale myślę, że pytanie PO nie było praktyczne (nikt tak naprawdę nie oszukiwałby w taki sposób w praktyce), ale teoretyczny.Po prostu napisz więcej testów. W końcu pisanie byłoby krótsze
niż
:-)
źródło
Pisanie testów „cheat” jest OK, dla wystarczająco małych wartości „OK”. Pamiętaj jednak, że testowanie jednostkowe jest zakończone tylko wtedy, gdy wszystkie testy zakończą się pomyślnie i nie będzie można napisać nowych testów, które się nie powiodą . Jeśli naprawdę chcesz mieć metodę CalculateFactorial, która zawiera kilka instrukcji if (lub jeszcze lepiej, dużą instrukcję switch / case :-) możesz to zrobić, a ponieważ masz do czynienia z liczbą o stałej precyzji wymagany kod implementacja tego jest skończona (choć prawdopodobnie dość duża i brzydka, a być może ograniczona przez kompilator lub ograniczenia systemowe dotyczące maksymalnego rozmiaru kodu procedury). W tym momencie, jeśli naprawdęNalegaj, aby wszystkie prace rozwojowe były prowadzone przez test jednostkowy. Możesz napisać test, który wymaga, aby kod obliczył wynik w czasie krótszym niż ten, który można osiągnąć, wykonując wszystkie gałęzie instrukcji if .
Zasadniczo TDD może pomóc Ci napisać kod, który poprawnie implementuje wymagania , ale nie może zmusić Cię do pisania dobrego kodu. To zależy od Ciebie.
Udostępnij i ciesz się.
źródło
Zgadzam się w 100% z sugestią Roberta Harveysa, tutaj nie chodzi tylko o zaliczenie testów, musisz również pamiętać o ogólnym celu.
Jako rozwiązanie twojego bólu: „zweryfikowano, że działa tylko z danym zestawem danych wejściowych”, zaproponowałbym użycie testów opartych na danych, takich jak teoria xunit. Podstawą tej koncepcji jest to, że pozwala ona łatwo tworzyć Specyfikacje wejść do wyjść.
W przypadku czynników czynnikowych test wyglądałby następująco:
Możesz nawet zaimplementować dostarczanie danych testowych (które zwraca
IEnumerable<Tuple<xxx>>
) i zakodować niezmiennik matematyczny, taki jak wielokrotne dzielenie przez n da n-1).Uważam, że tp to bardzo potężny sposób testowania.
źródło
Jeśli nadal możesz oszukiwać, testy nie są wystarczające. Napisz więcej testów! Na przykład postaram się dodać testy z danymi wejściowymi 1, -1, -1000, 0, 10, 200.
Niemniej jednak, jeśli naprawdę chcesz oszukiwać, możesz napisać niekończące się „jeśli-to”. W takim przypadku nic nie może pomóc oprócz przeglądu kodu. Wkrótce zostaniesz złapany na teście akceptacyjnym ( napisanym przez inną osobę! )
Problem z testami jednostkowymi polega na tym, że programiści postrzegają je jako niepotrzebną pracę. Prawidłowy sposób na ich zobaczenie to narzędzie, dzięki któremu poprawisz wynik swojej pracy. Jeśli więc utworzysz „jeśli-to”, nieświadomie wiesz, że istnieją inne przypadki do rozważenia. Oznacza to, że musisz napisać kolejne testy. I tak dalej, i tak dalej, dopóki nie uświadomisz sobie, że oszustwo nie działa i lepiej jest po prostu poprawnie kodować. Jeśli nadal czujesz, że nie skończyłeś, nie skończyłeś.
źródło
Sugerowałbym, że twój wybór testu nie jest najlepszym testem.
Zacząłbym od:
silnia (1) jako pierwszy test,
silnia (0) jako druga
silnia (-ve) jako trzecia
a następnie kontynuuj w nietrywialnych przypadkach
i zakończ skrzynką przelewową.
źródło
-ve
??