Czy zduplikowany kod jest bardziej tolerowany w testach jednostkowych?

113

Zrujnowałem kilka testów jednostkowych jakiś czas temu, kiedy przeszedłem i refaktoryzowałem je, aby były bardziej SUCHE - cel każdego testu nie był już jasny. Wydaje się, że istnieje kompromis między czytelnością testów a łatwością konserwacji. Jeśli zostawię zduplikowany kod w testach jednostkowych, będą bardziej czytelne, ale jeśli zmienię SUT , będę musiał wyśledzić i zmienić każdą kopię powielonego kodu.

Czy zgadzasz się, że ten kompromis istnieje? Jeśli tak, czy wolisz, aby testy były czytelne, czy możliwe do utrzymania?

Daryl Spitzer
źródło

Odpowiedzi:

68

Zduplikowany kod to zapach w kodzie testów jednostkowych, tak samo jak w innym kodzie. Jeśli masz zduplikowany kod w testach, trudniej jest zrefaktoryzować kod implementacji, ponieważ masz nieproporcjonalną liczbę testów do zaktualizowania. Testy powinny pomóc ci w pewnej refaktoryzacji, a nie być dużym obciążeniem, które utrudnia pracę nad testowanym kodem.

Jeśli powielanie jest w ustawieniach urządzenia, rozważ większe wykorzystanie tej setUpmetody lub zapewnienie większej (lub bardziej elastycznej) metody tworzenia .

Jeśli powielenie występuje w kodzie manipulującym SUT, zadaj sobie pytanie, dlaczego wiele tak zwanych testów „jednostkowych” wykonuje dokładnie tę samą funkcjonalność.

Jeśli duplikacja znajduje się w potwierdzeniach, być może potrzebujesz niektórych potwierdzeń niestandardowych . Na przykład, jeśli wiele testów ma ciąg stwierdzeń, takich jak:

assertEqual('Joe', person.getFirstName())
assertEqual('Bloggs', person.getLastName())
assertEqual(23, person.getAge())

W takim razie może potrzebujesz jednej assertPersonEqualmetody, aby móc pisać assertPersonEqual(Person('Joe', 'Bloggs', 23), person). (A może po prostu musisz przeciążyć operator równości Person).

Jak wspomniałeś, ważne jest, aby kod testowy był czytelny. W szczególności ważne jest, aby cel testu był jasny. Uważam, że jeśli wiele testów wygląda w większości tak samo (np. Trzy czwarte wierszy jest takich samych lub praktycznie takich samych), trudno jest dostrzec i rozpoznać istotne różnice bez uważnego ich przeczytania i porównania. Dlatego uważam, że refaktoryzacja w celu usunięcia duplikatów poprawia czytelność, ponieważ każda linia każdej metody testowej jest bezpośrednio związana z celem testu. Jest to o wiele bardziej pomocne dla czytelnika niż przypadkowa kombinacja wierszy, które są bezpośrednio istotne, i wierszy, które są tylko szablonami.

To powiedziawszy, czasami testy sprawdzają złożone sytuacje, które są podobne, ale wciąż znacząco różne, i trudno jest znaleźć dobry sposób na ograniczenie powielania. Kieruj się zdrowym rozsądkiem: jeśli uważasz, że testy są czytelne i jasno określają ich intencje, i czujesz się komfortowo, jeśli podczas refaktoryzacji kodu wywołanego przez testy musisz zaktualizować więcej niż teoretycznie minimalną liczbę testów, zaakceptuj niedoskonałość i przenieś na coś bardziej produktywnego. Zawsze możesz wrócić i zmodyfikować testy później, gdy nadejdzie inspiracja!

czarnogiełdziarz
źródło
30
„Zduplikowany kod to zapach w kodzie testów jednostkowych tak samo, jak w innym kodzie”. Nie. „Jeśli masz zduplikowany kod w testach, trudniej jest zrefaktoryzować kod implementacji, ponieważ masz nieproporcjonalną liczbę testów do zaktualizowania”. Dzieje się tak, ponieważ testujesz prywatny interfejs API zamiast publicznego interfejsu API.
15
Aby jednak zapobiec powielaniu kodu w testach jednostkowych, zwykle trzeba wprowadzić nową logikę. Nie sądzę, aby testy jednostkowe zawierały logikę, ponieważ wtedy potrzebne byłyby testy jednostkowe testów jednostkowych.
Petr Peller,
@ user11617 określ „prywatny interfejs API” i „publiczny interfejs API”. W moim rozumieniu publiczny interfejs API to interfejs API widoczny dla konsumentów ze świata zewnętrznego / stron trzecich i wyraźnie wersjonowany za pośrednictwem SemVer lub podobnego, wszystko inne jest prywatne. Przy tej definicji prawie wszystkie testy jednostkowe testują „prywatne API”, a zatem są bardziej wrażliwe na powielanie kodu, co moim zdaniem jest prawdą.
KolA
@KolA „Publiczny” nie oznacza klientów zewnętrznych - to nie jest internetowy interfejs API. Publiczne API klasy odnosi się do metod, które mają być używane przez kod klienta (który zazwyczaj nie powinien / nie powinien się zbytnio zmieniać) - ogólnie metody „publiczne”. Prywatny interfejs API odnosi się do logiki i metod, które są używane wewnętrznie. Nie powinno się ich otwierać spoza zajęć. Jest to jeden z powodów, dla których ważne jest prawidłowe hermetyzowanie logiki w klasie przy użyciu modyfikatorów dostępu lub konwencji w używanym języku.
Nathan,
@Nathan każdy pakiet biblioteki / dll / nuget ma klientów zewnętrznych, nie musi to być interfejs API sieci Web. To, o czym wspomniałem, to fakt, że bardzo często deklaruje się klasy publiczne i składowe, które nie powinny być używane bezpośrednio przez konsumentów biblioteki (lub w najlepszym przypadku tworzyć je wewnętrzne i dodawać adnotacje za pomocą InternalsVisibleToAttribute), aby umożliwić testom jednostkowym dotarcie do nich bezpośrednio. Prowadzi to do mnóstwa testów połączonych z implementacją i sprawia, że ​​są one bardziej obciążeniem niż zaletą
KolA
186

Czytelność jest ważniejsza w przypadku testów. Jeśli test się nie powiedzie, chcesz, aby problem był oczywisty. Deweloper nie powinien musieć przebrnąć przez wiele mocno podzielonego na czynniki kodu kodu testowego, aby dokładnie określić, co się nie powiodło. Nie chcesz, aby Twój kod testowy stał się tak złożony, że musisz pisać testy jednostkowe.

Jednak wyeliminowanie duplikacji jest zwykle dobrą rzeczą, o ile niczego nie przesłania, a wyeliminowanie duplikacji w testach może prowadzić do lepszego interfejsu API. Tylko upewnij się, że nie przekroczysz punktu malejących zwrotów.

Kristopher Johnson
źródło
xUnit i inne zawierają argument „wiadomość” w wywołaniach potwierdzenia. Dobry pomysł na umieszczenie znaczących zwrotów, aby umożliwić programistom szybkie znalezienie wyników testów, które nie zdały egzaminu.
seand
1
@seand Możesz spróbować wyjaśnić, co sprawdza twoja asercja, ale jeśli zawiedzie i zawiera nieco zasłonięty kod, programista i tak będzie musiał go rozwinąć. IMO Ważniejsze jest posiadanie kodu umożliwiającego samoopisywanie.
IgorK
1
@Kristopher,? Dlaczego ta publikacja jest wiki społeczności?
Pacerier,
@Pacerier Nie wiem. Kiedyś istniały skomplikowane zasady dotyczące automatycznego przekształcania się w wiki społeczności.
Kristopher Johnson
Dla czytelności raportów jest ważniejsza niż testy, szczególnie podczas przeprowadzania integracji lub testów typu end-to-end, scenariusze mogą być na tyle złożone, aby uniknąć odrobiny nawigowania, można znaleźć błąd, ale dla mnie błąd w raportach powinien wystarczająco dobrze wyjaśnij problem.
Anirudh
47

Kod i testy implementacyjne to różne zwierzęta, a zasady faktoringu mają do nich różne zastosowanie.

Powielony kod lub struktura jest zawsze zapachem w kodzie implementacji. Kiedy zaczniesz wdrażać schemat standardowy, musisz zrewidować swoje abstrakcje.

Z drugiej strony, kod testowy musi zachowywać poziom powielania. Duplikacja w kodzie testowym ma dwa cele:

  • Oddzielenie testów od produkcji. Zbyt częste łączenie testów może utrudnić zmianę pojedynczego testu, który zakończył się niepowodzeniem, który wymaga aktualizacji ze względu na zmianę umowy.
  • Utrzymywanie znaczenia testów w izolacji. Kiedy pojedynczy test kończy się niepowodzeniem, musi być dość proste, aby dowiedzieć się, co dokładnie testuje.

Zwykle ignoruję trywialne powielanie w kodzie testowym, o ile każda metoda testowa jest krótsza niż około 20 linii. Lubię, gdy rytm konfiguracja-uruchomienie-weryfikacja jest widoczny w metodach testowych.

Kiedy powielanie pojawia się w części testów „weryfikującej”, często korzystne jest zdefiniowanie niestandardowych metod potwierdzania. Oczywiście metody te muszą nadal testować jasno określoną zależność, która może być uwidoczniona w nazwie metody: assertPegFitsInHole-> dobra, assertPegIsGood-> zła.

Kiedy metody testowe stają się długie i powtarzalne, czasami wydaje mi się przydatne zdefiniowanie szablonów testów uzupełniających, które przyjmują kilka parametrów. Następnie rzeczywiste metody testowe sprowadzają się do wywołania metody szablonowej z odpowiednimi parametrami.

Jeśli chodzi o wiele rzeczy związanych z programowaniem i testowaniem, nie ma jednoznacznej odpowiedzi. Musisz rozwinąć gust, a najlepszym sposobem na to jest popełnianie błędów.

ddaa
źródło
8

Zgadzam się. Kompromis istnieje, ale jest różny w różnych miejscach.

Bardziej prawdopodobne jest, że zmienię zduplikowany kod w celu skonfigurowania stanu. Ale jest mniej prawdopodobne, że zmienimy tę część testu, która faktycznie ćwiczy kod. To powiedziawszy, jeśli ćwiczenie kodu zawsze zajmuje kilka wierszy kodu, to mogę pomyśleć, że jest to zapach i refaktoryzacja rzeczywistego testowanego kodu. A to poprawi czytelność i łatwość utrzymania zarówno kodu, jak i testów.

stucampbell
źródło
Myślę, że to dobry pomysł. Jeśli masz dużo duplikatów, sprawdź, czy możesz dokonać refaktoryzacji, aby utworzyć wspólne „urządzenie testowe”, w którym można uruchomić wiele testów. To wyeliminuje zduplikowany kod konfiguracji / dezaktywacji.
Outlaw Programmer
8

Możesz zmniejszyć liczbę powtórzeń, używając kilku różnych odmian metod testowych .

Jestem bardziej tolerancyjny na powtórzenia w kodzie testowym niż w kodzie produkcyjnym, ale czasami frustrowało mnie to. Kiedy zmieniasz projekt klasy i musisz cofnąć się i ulepszyć 10 różnych metod testowych, z których wszystkie wykonują te same kroki konfiguracji, jest to frustrujące.

Don Kirkby
źródło
6

Jay Fields ukuł zdanie, że „DSL powinny być wilgotne, a nie suche”, gdzie DAMP oznacza opisowe i znaczące wyrażenia . Myślę, że to samo dotyczy testów. Oczywiście zbyt duże powielanie jest złe. Ale usuwanie powielania za wszelką cenę jest jeszcze gorsze. Testy powinny działać jako specyfikacje ujawniające zamiary. Jeśli, na przykład, określisz ten sam element z kilku różnych punktów widzenia, należy spodziewać się pewnego powielenia.

Jörg W Mittag
źródło
3

Uwielbiam rspec z tego powodu:

Ma dwie rzeczy do pomocy -

  • wspólne grupy przykładowe do testowania typowego zachowania.
    możesz zdefiniować zestaw testów, a następnie „uwzględnić” go w swoich prawdziwych testach.

  • zagnieżdżone konteksty.
    zasadniczo możesz mieć metodę „konfiguracji” i „porzucenia” dla określonego podzbioru testów, a nie tylko dla wszystkich w klasie.

Im szybciej platformy testowe .NET / Java / inne zastosują te metody, tym lepiej (lub możesz użyć IronRuby lub JRuby do napisania testów, co osobiście uważam za lepszą opcję)

Orion Edwards
źródło
3

Uważam, że kod testowy wymaga podobnego poziomu inżynierii, który normalnie byłby stosowany w kodzie produkcyjnym. Z pewnością można argumentować za czytelnością i zgadzam się, że to ważne.

Jednak z mojego doświadczenia wynika, że ​​dobrze opracowane testy są łatwiejsze do odczytania i zrozumienia. Jeśli istnieje pięć testów, z których każdy wygląda tak samo, z wyjątkiem jednej zmiennej, która została zmieniona, i stwierdzenia na końcu, może być bardzo trudno znaleźć ten pojedynczy, różniący się element. Podobnie, jeśli zostanie uwzględnione w taki sposób, że widoczna jest tylko zmienna, która się zmienia, i asercja, wtedy łatwo jest natychmiast dowiedzieć się, co robi test.

Znalezienie odpowiedniego poziomu abstrakcji podczas testowania może być trudne i uważam, że warto to zrobić.

Kevin London
źródło
2

Myślę, że nie ma związku między bardziej zduplikowanym i czytelnym kodem. Myślę, że twój kod testowy powinien być tak dobry, jak twój inny kod. Niepowtarzający się kod jest bardziej czytelny niż kod zduplikowany, jeśli jest dobrze wykonany.

Paco
źródło
2

Idealnie byłoby, gdyby testy jednostkowe nie zmieniły się zbytnio po ich napisaniu, więc skłaniałbym się do czytelności.

Posiadanie tak dyskretnych testów jednostkowych, jak to tylko możliwe, pomaga również skupić się na testach na określonej funkcjonalności, do której są przeznaczone.

Mając to na uwadze, staram się ponownie używać pewnych fragmentów kodu, których używam w kółko, takich jak kod konfiguracji, który jest dokładnie taki sam w zestawie testów.

17 z 26
źródło
2

„refaktoryzowałem je, aby były bardziej SUCHE - cel każdego testu nie był już jasny”

Wygląda na to, że miałeś problem z refaktoryzacją. Zgaduję, ale jeśli okazało się mniej jasne, czy to nie oznacza, że ​​nadal masz więcej pracy, aby mieć dość eleganckie testy, które są całkowicie jasne?

Dlatego testy są podklasą UnitTest - dzięki czemu można zaprojektować dobre zestawy testów, które są poprawne, łatwe do zweryfikowania i przejrzyste.

W dawnych czasach mieliśmy narzędzia testowe, które korzystały z różnych języków programowania. Trudno było (lub niemożliwe) zaprojektować przyjemne, łatwe w obsłudze testy.

Masz pełną moc - niezależnie od używanego języka - Python, Java, C # - więc dobrze używaj tego języka. Możesz uzyskać dobrze wyglądający kod testowy, który jest przejrzysty i niezbyt zbędny. Nie ma kompromisu.

S.Lott
źródło