Jak skutecznie utrzymywać testy podczas przeprojektowywania?

14

Dobrze przetestowana baza kodowa ma wiele zalet, ale testowanie niektórych aspektów systemu daje bazę kodową odporną na niektóre rodzaje zmian.

Przykładem jest testowanie określonych wyników - np. Tekstu lub HTML. Testy są często (naiwnie?) Pisane, aby oczekiwać określonego bloku tekstu jako danych wyjściowych dla niektórych parametrów wejściowych lub aby wyszukać określone sekcje w bloku.

Zmiana zachowania kodu, aby spełnić nowe wymagania lub ponieważ testowanie użyteczności spowodowało zmianę interfejsu, również wymaga zmiany testów - być może nawet testów, które nie są konkretnymi testami jednostkowymi zmienianego kodu.

  • Jak zarządzasz pracą polegającą na wyszukiwaniu i przepisywaniu tych testów? Co jeśli nie możesz po prostu „uruchomić ich wszystkich i pozwolić, aby środowisko je uporządkowało”?

  • Jakie inne rodzaje testowanego kodu powodują zwykle kruche testy?

Alex Feinman
źródło
Czym różni się to znacznie od programmers.stackexchange.com/questions/5898/... ?
AShelly
4
To pytanie błędnie zadane na temat refaktoryzacji - testy jednostkowe powinny być niezmienne podczas refaktoryzacji.
Alex Feinman,

Odpowiedzi:

9

Wiem, że ludzie TDD będą nienawidzić tej odpowiedzi, ale dla mnie duża część to ostrożne wybranie, gdzie coś przetestować.

Jeśli zwariuję na punkcie testów jednostkowych na niższych poziomach, nie można wprowadzić znaczących zmian bez zmiany testów jednostkowych. Jeśli interfejs nigdy nie jest narażony i nie jest przeznaczony do ponownego użycia poza aplikacją, jest to po prostu niepotrzebny narzut na to, co w innym przypadku mogłoby być szybką zmianą.

I odwrotnie, jeśli to, co próbujesz zmienić, zostanie ujawnione lub ponownie użyte w każdym z tych testów, które będziesz musiał zmienić, jest dowodem na coś, co możesz złamać gdzie indziej.

W niektórych projektach może to oznaczać zaprojektowanie testów od poziomu akceptacji w dół, a nie od testów jednostkowych w górę. oraz mniej testów jednostkowych i więcej testów stylu integracji.

Nie oznacza to, że nadal nie można zidentyfikować pojedynczej funkcji i kodu, dopóki ta funkcja nie spełni kryteriów akceptacji. Oznacza to po prostu, że w niektórych przypadkach nie kończy się mierzeniem kryteriów akceptacji testami jednostkowymi.

Rachunek
źródło
Myślę, że chciałeś napisać „poza modułem”, a nie „poza aplikacją”.
SamB
SamB, to zależy. Jeśli interfejs jest wewnętrzny dla kilku miejsc z jedną aplikacją, ale nie jest publiczny, rozważę przetestowanie na wyższym poziomie, jeśli uważam, że interfejs może być niestabilny.
Bill
Przekonałem się, że to podejście jest bardzo kompatybilne z TDD. Lubię zaczynać w górnych warstwach aplikacji bliżej użytkownika końcowego, aby móc zaprojektować niższe warstwy, wiedząc, w jaki sposób górne warstwy muszą korzystać z niższych warstw. Zasadniczo budowanie z góry na dół pozwala na dokładniejsze zaprojektowanie interfejsu między warstwami.
Greg Burghardt
4

Właśnie zakończyłem gruntowny przegląd stosu SIP, przepisując cały transport TCP. (Był to prawie refaktor, na dość dużą skalę, w stosunku do większości refaktoryzacji).

W skrócie, istnieje TIdSipTcpTransport, podklasa TIdSipTransport. Wszystkie TIdSipTransports mają wspólny zestaw testów. Wewnątrz TIdSipTcpTransport znajdowało się kilka klas - mapa zawierająca pary połączenia / komunikatu inicjującego, wątkowych klientów TCP, wątkowy serwer TCP i tak dalej.

Oto co zrobiłem:

  • Usunąłem klasy, które zamierzałem zastąpić.
  • Usunięto zestawy testów dla tych klas.
  • Pozostawiono zestaw testowy specyficzny dla TIdSipTcpTransport (i wciąż był zestaw testowy wspólny dla wszystkich TIdSipTransports).
  • Przeszedł testy TIdSipTransport / TIdSipTcpTransport, aby upewnić się, że wszystkie zawiodły.
  • Skomentował wszystkie oprócz jednego testu TIdSipTransport / TIdSipTcpTransport.
  • Gdybym musiał dodać klasę, dodałbym, że piszę testy, aby zbudować wystarczającą funkcjonalność, którą przeszedł jedyny niekomentowany test.
  • Spłucz, spłucz, powtórz.

W ten sposób wiedziałem, co jeszcze muszę zrobić, w postaci skomentowanych testów (*) i wiedziałem, że nowy kod działa zgodnie z oczekiwaniami, dzięki nowym testom, które napisałem.

(*) Naprawdę, nie musisz ich komentować. Po prostu ich nie uruchamiaj; 100 nieudanych testów nie jest zbyt zachęcające. Ponadto w mojej konkretnej konfiguracji skompilowanie mniejszej liczby testów oznacza szybszą pętlę test-zapis-refaktor.

Frank Shearar
źródło
Zrobiłem to też kilka miesięcy temu i działało to całkiem dobrze dla mnie. Jednak nie mogłem bezwzględnie zastosować tej metody podczas parowania z kolegą w gruntownym przeprojektowaniu naszego modułu modelu domeny (co z kolei spowodowało przeprojektowanie wszystkich innych modułów w projekcie).
Marco Ciambrone
3

Kiedy testy są kruche, zwykle stwierdzam, że testuję niewłaściwą rzecz. Weźmy na przykład dane wyjściowe HTML. Jeśli sprawdzisz rzeczywiste wyjście HTML, twój test będzie kruchy. Ale nie interesuje Cię faktyczny wynik, interesuje Cię, czy przekazuje informacje, które powinien. Niestety, zrobienie tego wymaga stwierdzenia na temat zawartości mózgu użytkownika i dlatego nie można tego zrobić automatycznie.

Możesz:

  • Wygeneruj HTML jako test dymu, aby upewnić się, że rzeczywiście działa
  • Użyj systemu szablonów, aby przetestować procesor szablonów i dane wysłane do szablonu, bez testowania samego szablonu.

To samo dzieje się z SQL. Jeśli potwierdzisz faktyczny SQL, twoje klasy spróbują sprawić, że będziesz miał kłopoty. Naprawdę chcesz potwierdzić wyniki. Dlatego podczas testów jednostkowych używam bazy danych pamięci SQLITE, aby upewnić się, że mój SQL faktycznie robi to, co powinien.

Winston Ewert
źródło
Może to również pomóc w użyciu strukturalnego HTML.
SamB
@SamB z pewnością by to pomogło, ale nie sądzę, że to całkowicie rozwiąże problem
Winston Ewert
oczywiście, że nie, nic nie może :-)
SamB
-1

Najpierw utwórz NOWY interfejs API, który będzie działał tak, jak chcesz. Jeśli zdarza się, że ten nowy interfejs API ma taką samą nazwę jak interfejs OLDER API, to dołączam nazwę _NEW do nowej nazwy interfejsu API.

int DoSomethingInterestingAPI ();

staje się:

int DoSomethingInterestingAPI_NEW (int takes_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (what_default_mimics_the_old_API); OK - na tym etapie - wszystkie testy regresji 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ć).

Dzięki takiemu 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 jakiś czas nowego i starego kodu.

Oto dobry (twardy) przykład tego podejścia w praktyce. Miałem funkcję BitSubstring () - w której zastosowałem podejście polegające na tym, że trzecim parametrem jest LICZBA bitów w podciągu. Aby zachować spójność z innymi interfejsami API i wzorcami w C ++, chciałem przełączyć na początek / koniec jako argumenty funkcji.

https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0

Utworzyłem funkcję BitSubstring_NEW z nowym API i zaktualizowałem cały mój kod, aby z niej korzystać (pozostawiając NIE WIĘCEJ POŁĄCZEŃ dla BitSubString). Ale zostawiłem implementację na kilka wydań (miesiące) - i zaznaczyłem, że jest przestarzała - aby każdy mógł przełączyć się na BitSubString_NEW (i wtedy zmienić argument z stylu liczenia na początek / koniec).

NASTĘPNIE - kiedy przejście zostało zakończone, zrobiłem kolejne zatwierdzenie, usuwając BitSubString () i zmieniając nazwę BitSubString_NEW-> BitSubString () (i przestałem używać nazwy BitSubString_NEW).

Lewis Pringle
źródło
Nigdy nie dodawaj sufiksów, które nie mają znaczenia lub są samorzutne dla nazwisk. Zawsze staraj się nadawać sensowne nazwy.
Basilevs,
Całkowicie przegapiłeś punkt. Po pierwsze - nie są to przyrostki, które „nie mają znaczenia”. Mają one znaczenie, że interfejs API przechodzi ze starszego na nowszy. W rzeczywistości to jest sedno PYTANIA, na które odpowiadałem, i cały sedno odpowiedzi. Nazwy WYRAŹNIE komunikują, który jest OLD API, który jest NOWYMI API i który jest docelową nazwą API po zakończeniu przejścia. ORAZ - sufiksy _OLD / _NEW są tymczasowe - TYLKO podczas zmiany interfejsu API.
Lewis Pringle
Powodzenia z wersją API NEW_NEW_3 trzy lata później.
Basilevs,