W innym pytaniu ujawniono, że jednym z problemów związanych z TDD jest utrzymywanie pakietu testowego w synchronizacji z bazą kodu podczas i po refaktoryzacji.
Teraz jestem wielkim fanem refaktoryzacji. Nie zamierzam rezygnować z TDD. Ale doświadczyłem również problemów z testami napisanymi w taki sposób, że niewielkie refaktoryzowanie prowadzi do wielu niepowodzeń testów.
Jak uniknąć przełamywania testów podczas refaktoryzacji?
- Czy piszesz testy „lepiej”? Jeśli tak, czego powinieneś szukać?
- Czy unikasz pewnych rodzajów refaktoryzacji?
- Czy istnieją narzędzia do refaktoryzacji testów?
Edycja: Napisałem nowe pytanie, w którym zadałem pytanie , co chciałem zadać (ale zachowałem to jako ciekawy wariant).
development-process
tdd
refactoring
Alex Feinman
źródło
źródło
Odpowiedzi:
To, co próbujesz zrobić, nie jest tak naprawdę refaktoryzacją. Dzięki refaktoryzacji z definicji nie zmieniasz tego, co robi twoje oprogramowanie, zmieniasz, jak to robi.
Rozpocznij od wszystkich zielonych testów (wszystkie zaliczenia), a następnie dokonaj modyfikacji „pod maską” (np. Przenieś metodę z klasy pochodnej do bazy, wyodrębnij metodę lub obuduj kompozyt za pomocą Konstruktora itp.). Twoje testy wciąż powinny przejść.
To, co opisujesz, wydaje się nie refaktoryzować, ale przeprojektować, co również zwiększa funkcjonalność testowanego oprogramowania. TDD i refaktoryzacja (jak próbowałem to tutaj zdefiniować) nie są w konflikcie. Nadal możesz refaktoryzować (zielono-zielono) i zastosować TDD (czerwono-zielony), aby rozwinąć funkcjonalność „delta”.
źródło
Jedną z zalet posiadania testów jednostkowych jest to, że możesz pewnie refaktoryzować.
Jeśli refaktoryzacja nie zmieni interfejsu publicznego, wówczas pozostawiasz testy jednostkowe w niezmienionej postaci i upewniasz się, że po refaktoryzacji wszystkie przejdą pomyślnie.
Jeśli refaktoryzacja zmieni interfejs publiczny, najpierw należy przepisać testy. Refaktoryzuj do momentu przejścia nowych testów.
Nigdy nie uniknęłbym żadnego refaktoryzacji, ponieważ to psuje testy. Pisanie testów jednostkowych może być kłopotem w tyłku, ale na dłuższą metę jest warte bólu.
źródło
W przeciwieństwie do innych odpowiedzi, należy zauważyć, że niektóre sposoby testowania mogą stać się kruche, gdy testowany system (SUT) zostanie zrefaktoryzowany, jeśli test jest whitebox.
Jeśli korzystam z frameworka, który weryfikuje kolejność metod wywoływanych w próbkach (gdy kolejność jest nieistotna, ponieważ wywołania są wolne od efektów ubocznych); wtedy jeśli mój kod jest czystszy z tymi wywołaniami metod w innej kolejności, a ja dokonam refaktoryzacji, mój test się zepsuje. Ogólnie, makiety mogą wprowadzać kruchość do testów.
Jeśli sprawdzam wewnętrzny stan mojego SUT, ujawniając jego prywatnych lub chronionych członków (moglibyśmy użyć „przyjaciela” w Visual Basic lub eskalować poziom dostępu „wewnętrzny” i użyć „internalsvisibleto” w języku c #; w wielu językach OO, w tym c # mogłaby zostać użyta „ podklasa specyficzna dla testu ”), wtedy nagle stan wewnętrzny klasy będzie miał znaczenie - być może refaktoryzujesz klasę jako czarną skrzynkę, ale testy białej skrzynki zakończą się niepowodzeniem. Załóżmy, że jedno pole jest ponownie używane do oznaczania różnych rzeczy (nie jest to dobra praktyka!), Gdy stan SUT zmienia się - jeśli podzielimy je na dwa pola, może być konieczne przepisanie zepsutych testów.
Podklasy specyficzne dla testu można również wykorzystać do testowania metod chronionych - co może oznaczać, że refaktor z punktu widzenia kodu produkcyjnego jest przełomową zmianą z punktu widzenia kodu testowego. Przeniesienie kilku linii do lub z chronionej metody może nie wywoływać efektów ubocznych, ale przerwać test.
Jeśli użyję „ haków testowych ” lub jakiegokolwiek innego kodu kompilacyjnego specyficznego dla testu lub warunkowego, może być trudno zapewnić, że testy nie zostaną przerwane z powodu delikatnych zależności od wewnętrznej logiki.
Aby zapobiec sprzężeniu testów z intymnymi wewnętrznymi szczegółami SUT, pomocne może być:
Wszystkie powyższe punkty są przykładami sprzężenia białych skrzynek zastosowanych w testach. Aby całkowicie uniknąć refaktoryzacji testów niszczenia, należy zastosować testowanie SUT w czarnej skrzynce.
Zastrzeżenie: W celu omówienia refaktoryzacji używam tego słowa nieco szerzej, aby uwzględnić zmianę implementacji wewnętrznej bez widocznych efektów zewnętrznych. Niektórzy puryści mogą się nie zgadzać i odnoszą się wyłącznie do książki Martina Fowlera i Kenta Becka Refaktoryzacja - która opisuje operacje refaktoryzacji atomowej.
W praktyce staramy się podejmować nieco większe niełamliwe kroki niż opisane tam operacje atomowe, aw szczególności zmiany, które powodują, że kod produkcyjny zachowuje się identycznie z zewnątrz, mogą nie pozostawić pozytywnych wyników testów. Ale myślę, że sprawiedliwym jest włączenie „algorytmu zastępczego do innego algorytmu, który ma identyczne zachowanie” jako refaktora, i myślę, że Fowler się z tym zgadza. Sam Martin Fowler mówi, że refaktoryzacja może przerwać testy:
źródło
Jeśli twoje testy zostaną przerwane podczas refaktoryzacji, to z definicji nie jest to refaktoryzacja, która oznacza „zmianę struktury programu bez zmiany jego zachowania”.
Czasami musisz zmienić zachowanie swoich testów. Być może musisz połączyć ze sobą dwie metody (powiedzmy, bind () i listen () na nasłuchującej klasie gniazda TCP), więc inne części twojego kodu próbują i nie używają teraz zmienionego API. Ale to nie jest refaktoryzacja!
źródło
Myślę, że problem z tym pytaniem polega na tym, że różni ludzie inaczej interpretują słowo „refaktoryzacja”. Myślę, że najlepiej jest dokładnie zdefiniować kilka rzeczy, które prawdopodobnie masz na myśli:
Jak zauważyła już inna osoba, jeśli utrzymujesz interfejs API bez zmian, a wszystkie testy regresji działają na publicznym interfejsie API, nie powinieneś mieć problemów. Refaktoryzacja nie powinna powodować żadnych problemów. Wszelkie nieudane testy EITHER oznaczają, że Twój stary kod zawierał błąd, a test jest zły lub nowy kod zawiera błąd.
Ale to całkiem oczywiste. PRAWDOPODOBNIE rozumiesz przez refaktoryzację, że zmieniasz interfejs API.
Pozwól, że odpowiem, jak do tego podejść!
Najpierw utwórz NOWY interfejs API, który będzie działał tak, jak chcesz. Jeśli zdarzy się, że ten nowy interfejs API ma taką samą nazwę jak interfejs OLDER API, wówczas dołączam nazwę _NEW do nowej nazwy interfejsu API.
int DoSomethingInterestingAPI ();
staje się:
OK - na tym etapie - wszystkie twoje testy regresyjne przechodzą pomyślnie - używając nazwy DoSomethingInterestingAPI ().
NASTĘPNIE, przejrzyj kod i zmień wszystkie wywołania DoSomethingInterestingAPI () na odpowiedni wariant DoSomethingInterestingAPI_NEW (). Obejmuje to aktualizację / przepisywanie dowolnych części testów regresji, które należy zmienić, aby korzystać z nowego interfejsu API.
NASTĘPNY, oznacz DoSomethingInterestingAPI_OLD () jako [[przestarzałe ()]]. Trzymaj się przestarzałego interfejsu API tak długo, jak chcesz (dopóki nie zaktualizujesz bezpiecznie całego kodu, który może od niego zależeć).
Przy takim podejściu wszelkie niepowodzenia w testach regresji są po prostu błędami w teście regresji lub identyfikują błędy w kodzie - dokładnie tak, jak byś tego chciał. Ten etapowy proces przeglądu interfejsu API poprzez jawne tworzenie wersji API _NEW i _OLD pozwala na współdziałanie przez pewien czas nowego i starego kodu.
źródło
Zakładam, że twoje testy jednostkowe mają szczegółowość, którą nazwałbym „głupimi” :) tj. Testują absolutne minucje każdej klasy i funkcji. Odejdź od narzędzi do generowania kodu i pisz testy, które dotyczą większej powierzchni, a następnie możesz refaktoryzować elementy wewnętrzne tak, jak chcesz, wiedząc, że interfejsy do aplikacji nie uległy zmianie, a twoje testy nadal działają.
Jeśli chcesz mieć testy jednostkowe, które sprawdzają każdą metodę, spodziewaj się, że będziesz musiał je jednocześnie refaktoryzować.
źródło
Trudno jest sprzęgać . Wszelkie testy mają pewien stopień sprzężenia ze szczegółami implementacji, ale testy jednostkowe (niezależnie od tego, czy jest to TDD, czy nie) są szczególnie złe, ponieważ zakłócają wewnętrzne: więcej testów jednostkowych oznacza więcej kodów połączonych z jednostkami, tj. Podpisy metod / dowolny inny interfejs publiczny jednostek - przynajmniej.
„Jednostki” z definicji są szczegółami implementacji niskiego poziomu, interfejs jednostek może i powinien się zmieniać / dzielić / scalać i w inny sposób mutować w miarę ewolucji systemu. Mnóstwo testów jednostkowych może w rzeczywistości bardziej utrudnić tę ewolucję, niż to pomaga.
Jak uniknąć przełamywania testów podczas refaktoryzacji? Unikaj sprzężenia. W praktyce oznacza to unikanie jak największej liczby testów jednostkowych i preferowanie testów wyższego poziomu / integracji bardziej niezależnych od szczegółów implementacji. Pamiętaj jednak, że nie ma srebrnej kuli, testy nadal muszą być powiązane z czymś na pewnym poziomie, ale idealnie powinien to być interfejs, który jest jawnie wersjonowany przy użyciu Wersji semantycznej, tj. Zwykle na opublikowanym poziomie interfejsu API / aplikacji (nie chcesz robić SemVer dla każdej jednostki w twoim rozwiązaniu).
źródło
Twoje testy są zbyt ściśle powiązane z implementacją, a nie z wymaganiami.
zastanów się nad napisaniem testów z takimi komentarzami:
w ten sposób nie można refaktoryzować znaczenia poza testami.
źródło