Przeczytałem kilka odpowiedzi na pytania o podobnej treści, na przykład „Jak utrzymać testy jednostkowe podczas refaktoryzacji?”. W moim przypadku scenariusz jest nieco inny, ponieważ otrzymałem projekt do przeglądu i dostosowania go do niektórych standardów, które obecnie posiadamy, obecnie nie ma żadnych testów dla tego projektu!
Zidentyfikowałem szereg rzeczy, które moim zdaniem można by zrobić lepiej, takich jak NIE mieszanie kodu typu DAO w warstwie usługowej.
Przed refaktoryzacją dobrym pomysłem było napisanie testów dla istniejącego kodu. Wydaje mi się, że problem polega na tym, że kiedy dokonam refaktoryzacji, testy te zostaną przerwane, ponieważ zmieniam miejsce, w którym wykonywana jest pewna logika, a testy będą pisane z myślą o poprzedniej strukturze (wyśmiewane zależności itp.)
W moim przypadku jaki byłby najlepszy sposób postępowania? Kusi mnie, aby napisać testy wokół refaktoryzowanego kodu, ale jestem świadomy, że istnieje ryzyko, że mogę niepoprawnie refaktoryzować rzeczy, które mogłyby zmienić pożądane zachowanie.
Niezależnie od tego, czy jest to refaktoryzacja, czy przeprojektowanie, cieszę się, że rozumiem te warunki, które mają zostać poprawione, obecnie pracuję nad następującą definicją refaktoryzacji „Dzięki refaktoryzacji z definicji nie zmieniasz tego, co robi twoje oprogramowanie, zmieniasz, jak to się robi. ”. Więc nie zmieniam tego, co robi oprogramowanie, zmieniam, jak / gdzie to robi.
Równie dobrze widzę argument, że jeśli zmieniam podpis metod, które można by uznać za przeprojektowanie.
Oto krótki przykład
MyDocumentService.java
(obecny)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
DataResultSet rs = documentDAO.findAllDocuments();
List<Document> documents = new ArrayList<>();
for(DataObject do: rs.getRows()) {
//get row data create new document add it to
//documents list
}
return documents;
}
}
MyDocumentService.java
(przebudowane / przeprojektowane cokolwiek)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
//Code dealing with DataResultSet moved back up to DAO
//DAO now returns a List<Document> instead of a DataResultSet
return documentDAO.findAllDocuments();
}
}
źródło
Odpowiedzi:
Szukasz testów sprawdzających regresje . tj. łamanie niektórych istniejących zachowań. Zacznę od ustalenia, na jakim poziomie to zachowanie pozostanie takie samo, a interfejs sterujący tym zachowaniem pozostanie taki sam, i zacznę testować w tym momencie.
Masz teraz kilka testów, które potwierdzą, że cokolwiek zrobisz poniżej tego poziomu, twoje zachowanie pozostanie takie samo.
Masz całkowitą rację, kwestionując sposób, w jaki testy i kod mogą być zsynchronizowane. Jeśli twój interfejs do komponentu pozostaje taki sam, możesz napisać wokół niego test i zapewnić te same warunki dla obu implementacji (podczas tworzenia nowej implementacji). Jeśli tak nie jest, musisz zaakceptować, że test nadmiarowego komponentu jest testem nadmiarowym.
źródło
Zalecaną praktyką jest zacząć od napisania „szczegółowych testów”, które testują bieżące zachowanie kodu, być może włączając w to błędy, ale bez konieczności schodzenia w szaleństwo rozróżniania, czy dane zachowanie, które narusza wymagania dokumentów, jest błędem, obejście problemu, którego nie znasz, lub stanowi nieudokumentowaną zmianę wymagań.
Najbardziej sensowne jest, aby te szczegółowe testy były na wysokim poziomie, tj. Testy integracyjne, a nie jednostkowe, aby działały po rozpoczęciu refaktoryzacji.
Ale niektóre refaktoryzacje mogą być konieczne, aby kod był testowalny - po prostu uważaj, aby trzymać się „bezpiecznych” refaktoryzacji. Na przykład w prawie wszystkich przypadkach metody, które są prywatne, można upublicznić, nie niszcząc niczego.
źródło
Sugeruję - jeśli jeszcze tego nie zrobiłeś - przeczytanie zarówno Efektywnej pracy ze starszym kodem, jak i Refaktoryzacja - Poprawa projektu istniejącego kodu .
Niekoniecznie postrzegam to jako problem: napisz testy, zmień strukturę kodu, a następnie dostosuj również strukturę testu . Dzięki temu uzyskasz bezpośrednią informację zwrotną, czy twoja nowa struktura jest rzeczywiście lepsza niż stara, ponieważ jeśli tak, to skorygowane testy będą łatwiejsze do napisania (a zatem zmiana testów powinna być stosunkowo prosta, zmniejszając ryzyko nowo wprowadzonego błąd zdał testy).
Ponadto, jak już napisali inni: Nie pisz zbyt szczegółowych testów (przynajmniej nie na początku). Staraj się pozostać na wysokim poziomie abstrakcji (dlatego twoje testy będą prawdopodobnie lepiej scharakteryzowane jako testy regresji, a nawet testy integracji).
źródło
Nie pisz ścisłych testów jednostkowych, w których kpisz ze wszystkich zależności. Niektórzy powiedzą, że to nie są prawdziwe testy jednostkowe. Ignoruj ich. Te testy są przydatne i to jest ważne.
Spójrzmy na twój przykład:
Twój test prawdopodobnie wygląda mniej więcej tak:
Zamiast kpić z Documentexe, kpić z jego zależności:
Teraz można przejść od logiki
MyDocumentService
doDocumentDao
bez łamania testów. Testy pokażą, że funkcjonalność jest taka sama (o ile ją przetestowałeś).źródło
Jak mówisz, jeśli zmienisz zachowanie, będzie to transformacja, a nie refaktor. Na jakim poziomie zmieniasz zachowanie, to robi różnicę.
Jeśli nie ma formalnych testów na najwyższym poziomie, spróbuj znaleźć zestaw wymagań, które klienci (wywołujący kod lub ludzie) muszą pozostać bez zmian po przeprojektowaniu kodu, aby mógł zostać uznany za działający. To jest lista przypadków testowych, które musisz zaimplementować.
Aby odpowiedzieć na twoje pytanie dotyczące zmiany implementacji wymagających zmiany przypadków testowych, proponuję spojrzeć na TDD Detroit (klasyczne) vs Londyn (mockist). Martin Fowler mówi o tym w swoim świetnym artykule. Drwiny nie są skrótami, ale wiele osób ma opinie. Jeśli zaczniesz od najwyższego poziomu, na którym twoje efekty zewnętrzne nie mogą się zmienić, i zejdziesz na dół, wymagania powinny pozostać dość stabilne, dopóki nie dojdziesz do poziomu, który naprawdę musi się zmienić.
Bez testów będzie to trudne i możesz rozważyć uruchomienie klientów za pomocą podwójnych ścieżek kodu (i rejestrowanie różnic), dopóki nie upewnisz się, że nowy kod robi dokładnie to, co powinien.
źródło
Oto moje podejście. Ma koszt pod względem czasu, ponieważ jest to test refaktorski w 4 fazach.
To, co zamierzam ujawnić, może lepiej pasować do komponentów o większej złożoności niż ta przedstawiona w przykładzie pytania.
W każdym razie strategia jest ważna dla dowolnego kandydata na komponent, który ma zostać znormalizowany przez interfejs (DAO, usługi, kontrolery, ...).
1. Interfejs
Zbierzmy wszystkie publiczne metody z MyDocumentService i połączmy je wszystkie w jeden interfejs. Na przykład. Jeśli już istnieje, użyj tego zamiast ustawiania nowego .
Następnie zmuszamy MyDocumentService do wdrożenia tego nowego interfejsu.
Na razie w porządku. Nie wprowadzono większych zmian, dotrzymaliśmy aktualnego kontraktu, a behaivos pozostaje nietknięte.
2. Test jednostkowy starszego kodu
Tutaj mamy ciężką pracę. Aby skonfigurować pakiet testowy. Powinniśmy ustawić jak najwięcej przypadków: przypadki udane, a także przypadki błędów. Te ostatnie służą dobrej jakości wyniku.
Teraz zamiast testować MyDocumentService , będziemy używać interfejsu jako testowanej umowy.
Nie będę wchodził w szczegóły, więc wybacz mi, jeśli mój kod wygląda na zbyt prosty lub zbyt agnostyczny
Ten etap trwa dłużej niż jakikolwiek inny w tym podejściu. I to jest najważniejsze, ponieważ wyznaczy punkt odniesienia dla przyszłych porównań.
Uwaga: Z powodu braku większych zmian, zachowanie nie zostało zmienione. Proponuję zrobić tag tutaj w SCM. Tag lub gałąź nie ma znaczenia. Po prostu zrób wersję.
Chcemy go do wycofywania, porównywania wersji i może być do równoległego wykonywania starego kodu i nowego.
3. Refaktoryzacja
Refactor zostanie wdrożony w nowym komponencie. Nie dokonamy żadnych zmian w istniejącym kodzie. Pierwszy krok jest tak prosty, jak skopiowanie i wklejenie MyDocumentService i zmiana nazwy na CustomDocumentService (na przykład).
Nowa klasa nadal wdraża usługę DocumentService . Następnie przejdź i refaktoryzuj getAllDocuments () . (Zacznijmy od jednego. Refaktory)
Może to wymagać pewnych zmian w interfejsie / metodach DAO. Jeśli tak, nie zmieniaj istniejącego kodu. Zaimplementuj własną metodę w interfejsie DAO. Oznacz stary kod jako Przestarzały, a później dowiesz się, co należy usunąć.
Ważne jest, aby nie przerywać / zmieniać istniejącej implementacji. Chcemy wykonywać obie usługi równolegle, a następnie porównywać wyniki.
4. Aktualizacja DocumentServiceTestSuite
Ok, teraz łatwiejsza część. Aby dodać testy nowego komponentu.
Teraz mamy zarówno oldResult, jak i newResult, które zostały sprawdzone niezależnie, ale możemy je również porównać ze sobą. Ta ostatnia walidacja jest opcjonalna i zależy od wyniku. Być może nie jest to porównywalne.
Może nie robić zbyt wiele sensu, aby porównywać dwie kolekcje w ten sposób, ale byłoby ważne dla każdego innego rodzaju obiektu (pojos, encje modelu danych, DTO, opakowania, typy rodzime ...)
Notatki
Nie odważyłbym się powiedzieć, jak przeprowadzać testy jednostkowe ani jak używać fałszywych bibliotek. Nie mam odwagi powiedzieć, jak trzeba zrobić refaktor. Chciałem zaproponować globalną strategię. To, jak pójść naprzód, zależy od Ciebie. Wiesz dokładnie, jaki jest kod, jego złożoność i czy taka strategia jest warta wypróbowania. Liczą się tu fakty, takie jak czas i zasoby. Ważne jest również to, czego oczekujesz od tych testów w przyszłości.
Zacząłem moje przykłady od usługi i podążałem za DAO i tak dalej. Wchodzenie głęboko w poziomy zależności. Mniej więcej to może być opisany jako góra-dolnym strategii. Jednak w przypadku drobnych zmian / refaktorów ( takich jak ten pokazany w przykładzie trasy ) oddolne wykonanie zadania byłoby łatwiejsze. Ponieważ zakres zmian jest niewielki.
Wreszcie, to do Ciebie należy usunięcie przestarzałego kodu i przekierowanie starych zależności na nowe.
Usuń również przestarzałe testy i zadanie zostanie wykonane. Jeśli wersjonujesz stare rozwiązanie z jego testami, możesz sprawdzić i porównać się w dowolnym momencie.
W wyniku tak dużej ilości pracy przetestowano, sprawdzono i zaktualizowano starszy kod. Nowy kod, przetestowany, sprawdzony i gotowy do wersji.
źródło
tl; dr Nie pisz testów jednostkowych. Napisz testy na bardziej odpowiednim poziomie.
Biorąc pod uwagę roboczą definicję refaktoryzacji:
jest bardzo szerokie spektrum. Z jednej strony jest samodzielna zmiana konkretnej metody, być może wykorzystująca bardziej wydajny algorytm. Na drugim końcu jest portowanie na inny język.
Niezależnie od tego, jaki poziom refaktoryzacji / przeprojektowania jest wykonywany, ważne jest, aby mieć testy działające na tym poziomie lub wyższym.
Zautomatyzowane testy są często klasyfikowane według poziomu jako:
Testy jednostkowe - poszczególne elementy (klasy, metody)
Testy integracyjne - Interakcje między komponentami
Testy systemowe - kompletna aplikacja
Napisz poziom testu, który może przetrwać refaktoryzację w zasadzie nietknięty.
Myśleć:
źródło
Nie trać czasu na pisanie testów, które podpinają się w punktach, w których można spodziewać się, że interfejs zmieni się w niebanalny sposób. Jest to często znak, że próbujesz przetestować jednostki, które mają charakter „oparty na współpracy” - których wartość nie polega na tym, co robią sami, ale na tym, jak wchodzą w interakcję z wieloma blisko spokrewnionymi klasami, aby uzyskać cenne zachowanie . To , że zachowanie, które chcesz przetestować, co oznacza, że chcesz być testowanie na wyższym poziomie. Testowanie poniżej tego poziomu często wymaga dużo brzydkiego drwiny, a powstałe testy mogą bardziej hamować rozwój niż pomagać w obronie zachowania.
Nie przejmuj się zbytnio, czy robisz remont, przeprojektowanie, czy cokolwiek innego. Możesz wprowadzać zmiany, które na niższym poziomie stanowią przeprojektowanie wielu komponentów, ale na wyższym poziomie integracji po prostu stanowią refaktor. Chodzi o to, aby wyjaśnić, jakie zachowanie jest dla ciebie wartościowe, i bronić tego zachowania w miarę upływu czasu.
Podczas pisania testów warto zastanowić się - czy mógłbym łatwo opisać QA, właścicielowi produktu lub użytkownikowi, co ten test faktycznie testuje? Jeśli wydaje się, że opisanie testu byłoby zbyt ezoteryczne i techniczne, być może testujesz na niewłaściwym poziomie. Testuj w punktach / poziomach, które „mają sens”, i nie uszkadzaj kodu testami na każdym poziomie.
źródło
Twoim pierwszym zadaniem jest próba wymyślenia „idealnej sygnatury metody” do swoich testów. Staraj się, aby była to czysta funkcja . Powinno to być niezależne od testowanego kodu; jest to mała warstwa adaptera. Napisz swój kod do tej warstwy adaptera. Teraz, gdy refaktoryzujesz kod, musisz tylko zmienić warstwę adaptera. Oto prosty przykład:
Testy są dobre, ale testowany kod ma zły interfejs API. Mogę zmienić to bez zmiany testów, po prostu aktualizując warstwę adaptera:
Ten przykład wydaje się dość oczywistą rzeczą do zrobienia zgodnie z zasadą „Nie powtarzaj się”, ale w innych przypadkach może nie być tak oczywisty. Korzyść wykracza poza DRY - prawdziwą zaletą jest oddzielenie testów od testowanego kodu.
Oczywiście ta technika może nie być zalecana we wszystkich sytuacjach. Na przykład nie byłoby powodu pisać adapterów dla POCO / POJO, ponieważ tak naprawdę nie mają one interfejsu API, który mógłby się zmieniać niezależnie od kodu testowego. Również jeśli piszesz niewielką liczbę testów, stosunkowo duża warstwa adaptera byłaby prawdopodobnie zmarnowanym wysiłkiem.
źródło