Czy szpiegowanie sprawdzonych klas to zła praktyka?

14

Pracuję nad projektem, w którym połączenia wewnętrzne klasy są zwykle, ale wyniki są wielokrotnie proste. Przykład ( nie prawdziwy kod ):

public boolean findError(Set<Thing1> set1, Set<Thing2> set2) {
  if (!checkFirstCondition(set1, set2)) {
    return false;
  }
  if (!checkSecondCondition(set1, set2)) {
    return false;
  }
  return true;
}

Pisanie testów jednostkowych dla tego typu kodu jest naprawdę trudne, ponieważ chcę tylko przetestować system warunków, a nie implementację rzeczywistych warunków. (Robię to w osobnych testach.) W rzeczywistości byłoby lepiej, gdybym zdał funkcje, które implementują warunki, a w testach po prostu podałem jakiś próbny. Problemem w tym podejściu jest hałas: często używamy leków generycznych .

Działające rozwiązanie; ma jednak na celu uczynienie testowanego obiektu szpiegiem i wykpienie wywołań funkcji wewnętrznych.

systemUnderTest = Mockito.spy(systemUnderTest);
doReturn(true).when(systemUnderTest).checkFirstCondition(....);

Problem polega na tym, że implementacja SUT została skutecznie zmieniona i utrzymywanie synchronizacji testów z implementacją może być problematyczne. Czy to prawda? Czy istnieje najlepsza praktyka pozwalająca uniknąć spustoszenia wywołań metod wewnętrznych?

Zauważ, że mówimy o częściach algorytmu, więc podział go na kilka klas może nie być pożądaną decyzją.

allprog
źródło

Odpowiedzi:

15

Testy jednostkowe powinny traktować badane klasy jako czarne skrzynki. Liczy się tylko to, że jej publiczne metody zachowują się tak, jak się spodziewano. Sposób, w jaki klasa to osiąga za pomocą wewnętrznych metod państwowych i prywatnych, nie ma znaczenia.

Kiedy czujesz, że nie można w ten sposób tworzyć znaczących testów, jest to znak, że twoje klasy są zbyt potężne i robią za dużo. Powinieneś rozważyć przeniesienie części ich funkcjonalności do osobnych klas, które można testować osobno.

Philipp
źródło
1
Już dawno zrozumiałem pomysł testowania jednostek i napisałem kilka z nich z powodzeniem. Po prostu oszukuję, że coś wygląda na papierze prosto, wygląda gorzej w kodzie i wreszcie mam do czynienia z czymś, co ma naprawdę prosty interfejs, ale wymaga ode mnie kpienia z połowy świata wokół danych wejściowych.
allprog
@allprog Kiedy musisz dużo wyśmiewać, wydaje się, że masz zbyt wiele zależności między swoimi klasami. Czy próbowałeś zmniejszyć sprzężenie między nimi?
Philipp
@allprog, jeśli jesteś w takiej sytuacji, winny jest projekt klasy.
itsbruce
To model danych powoduje ból głowy. Musi być zgodny z zasadami ORM i wieloma innymi wymaganiami. Dzięki czystej logice biznesowej i kodowi bezstanowemu znacznie łatwiej jest poprawnie przeprowadzić testy jednostkowe.
allprog,
3
Testy jednostkowe niekoniecznie muszą obsługiwać SUT jako backbox. Dlatego nazywane są testami jednostkowymi. Kpiąc z zależności, mogę wpływać na środowisko, a żeby wiedzieć, co muszę kpić, muszę też znać niektóre elementy wewnętrzne. Ale oczywiście nie oznacza to, że SUT należy zmienić w jakikolwiek sposób. Szpiegowanie pozwala jednak na pewne zmiany.
allprog
4

Jeśli oba findError()i checkFirstCondition()itp. Są publicznymi metodami twojej klasy, to findError()jest to faktycznie fasada dla funkcjonalności, która jest już dostępna z tego samego API. Nie ma w tym nic złego, ale oznacza to, że musisz napisać dla niego testy, które są bardzo podobne do już istniejących testów. Ta duplikacja po prostu odzwierciedla duplikację w interfejsie publicznym. To nie jest powód, by traktować tę metodę inaczej niż inne.

Kilian Foth
źródło
Metody wewnętrzne są upubliczniane tylko dlatego, że muszą być testowalne i nie chcę podklasować SUT ani włączać testów jednostkowych do klasy SUT jako statycznej klasy wewnętrznej. Ale rozumiem o co ci chodzi. Nie mogłem jednak znaleźć dobrych wskazówek, aby uniknąć takich sytuacji. Samouczki zawsze utknęły na poziomie podstawowym, który nie ma nic wspólnego z prawdziwym oprogramowaniem. W przeciwnym razie powodem szpiegowania jest właśnie uniknięcie powielania kodu testowego i sprawienie, by jednostka testowa miała zasięg.
allprog,
3
Nie zgadzam się, że metody pomocnicze muszą być publiczne do prawidłowego testowania jednostkowego. Jeśli kontrakt metody stwierdza, że ​​sprawdza różne warunki, to nie ma nic złego w pisaniu kilku testów dla tej samej metody publicznej, po jednym dla każdego „podwykonawstwa”. Celem testów jednostkowych jest uzyskanie pokrycia całego kodu, a nie powierzchowne pokrycie metod publicznych za pomocą korespondencji testowej metody 1: 1.
Kilian Foth,
Używanie tylko publicznego interfejsu API do testowania jest wielokrotnie bardziej skomplikowane niż testowanie wewnętrznych elementów jeden po drugim. Nie twierdzę, rozumiem, że to podejście nie jest najlepsze i ma swoją perspektywę, którą pokazuje moje pytanie. Największym problemem jest to, że funkcji nie można komponować w Javie, a obejścia są bardzo zwięzłe. Ale wydaje się, że nie ma innego rozwiązania dla prawdziwych testów jednostkowych.
allprog,
4

Testy jednostkowe powinny przetestować umowę; to dla nich jedyna ważna rzecz. Testowanie czegokolwiek, co nie jest częścią umowy, to nie tylko strata czasu, ale także potencjalne źródło błędów. Za każdym razem, gdy zobaczysz programistę zmieniającego testy, gdy zmieni szczegóły implementacji, powinny zadzwonić dzwonki alarmowe; ten programista może (celowo lub nie) ukrywać swoje błędy. Celowe testowanie szczegółów implementacji wymusza ten zły nawyk, zwiększając prawdopodobieństwo maskowania błędów.

Połączenia wewnętrzne są szczegółami implementacyjnymi i powinny być jedynie przydatne do pomiaru wydajności . Co zwykle nie jest zadaniem testów jednostkowych.

itsbruce
źródło
Brzmi wspaniale. Ale w rzeczywistości „ciąg”, który muszę wpisać i nazwać kodem, jest w języku, który niewiele wie o funkcjach. Teoretycznie mogę łatwo opisać problem i dokonać tu i tam zamiany, aby go uprościć. W kodzie muszę dodać dużo szumu syntaktycznego, aby osiągnąć tę elastyczność, która powstrzymuje mnie od korzystania z niego. Jeśli metoda azawiera wywołanie metody bw tej samej klasie, wówczas testy amuszą obejmować testy b. I nie ma sposobu, aby to zmienić, dopóki bnie zostanie przekazane ajako parametr. Ale nie ma innego rozwiązania, rozumiem.
allprog,
1
Jeśli bjest częścią interfejsu publicznego, i tak należy go przetestować. Jeśli tak nie jest, nie trzeba go testować. Jeśli podałeś go do publicznej wiadomości tylko dlatego, że chciałeś go przetestować, zrobiłeś źle.
itsbruce
Zobacz mój komentarz do odpowiedzi @ Philip. Nie wspomniałem jeszcze, ale model danych jest źródłem zła. Czysty, bezpaństwowy kod to bułka z masłem.
allprog,
2

Po pierwsze zastanawiam się, co jest trudne do przetestowania w przykładowej funkcji, którą napisałeś? O ile widzę, możesz po prostu przekazać różne dane wejściowe i sprawdzić, czy zwracana jest poprawna wartość logiczna. czego mi brakuje?

Jeśli chodzi o szpiegów, rodzaj tak zwanych testów „białych skrzynek”, w których wykorzystuje się szpiegów i kpiny, jest o rząd wielkości większy do napisania, nie tylko dlatego, że jest o wiele więcej kodu testowego do napisania, ale za każdym razem implementacja jest zmieniony, musisz również zmienić testy (nawet jeśli interfejs pozostaje taki sam). Ten rodzaj testowania jest również mniej niezawodny niż testowanie czarnej skrzynki, ponieważ musisz upewnić się, że cały ten dodatkowy kod testowy jest poprawny, i chociaż możesz ufać, że testy jednostkowe czarnej skrzynki zakończą się niepowodzeniem, jeśli nie będą zgodne z interfejsem , nie można ufać temu w kwestii nadużywania kodu, ponieważ czasami test nawet nie testuje zbyt wiele prawdziwego kodu - tylko symulacje. Jeśli próby są niepoprawne, istnieje prawdopodobieństwo, że testy się powiodą, ale kod nadal jest uszkodzony.

Każdy, kto ma doświadczenie w testowaniu białych skrzynek, może powiedzieć ci, że pisanie i utrzymywanie jest uciążliwe. W połączeniu z faktem, że są mniej niezawodne, testy białych skrzynek są po prostu znacznie gorsze w większości przypadków.

BT
źródło
Dziękuję za notatkę. Przykładowa funkcja jest o rząd wielkości prostsza niż cokolwiek, co trzeba napisać w złożonym algorytmie. W rzeczywistości pytanie brzmi bardziej: czy problematyczne jest testowanie algorytmów szpiegujących w kilku częściach. To nie jest kod stanowy, wszystkie stany są rozdzielone na argumenty wejściowe. Problem polega na tym, że chcę przetestować złożoną funkcję w tym przykładzie bez konieczności podawania rozsądnych parametrów dla funkcji podrzędnych.
allprog
Wraz z nadejściem programowania funkcjonalnego w Javie 8 stało się to nieco bardziej eleganckie, ale utrzymanie funkcjonalności w jednej klasie może być lepszym wyborem w przypadku algorytmów niż wyodrębnianie różnych (samodzielnie nieprzydatnych) części do „użycia raz” klasy tylko ze względu na testowalność. Pod tym względem szpiedzy robią to samo, co drwiny, ale bez konieczności wizualnego wysadzania spójnego kodu. W rzeczywistości używany jest ten sam kod instalacyjny, co w przypadku prób. Lubię trzymać się z daleka od skrajności, każdy rodzaj testu może być odpowiedni w określonych miejscach. Testowanie w jakiś sposób jest o wiele lepsze niż nie. :)
allprog
„Chcę przetestować złożoną funkcję .. bez konieczności podawania rozsądnych parametrów dla funkcji podrzędnych” - Nie rozumiem, co masz na myśli. Które podfunkcje? Czy mówisz o funkcjach wewnętrznych używanych przez „funkcję złożoną”?
BT
Do tego przydaje się szpiegostwo w moim przypadku. Funkcje wewnętrzne są dość skomplikowane do kontrolowania. Nie z powodu kodu, ale dlatego, że implementują coś logicznie złożonego. Przenoszenie rzeczy w innej klasie jest naturalną opcją, ale same te funkcje w ogóle nie są przydatne. Dlatego lepszym rozwiązaniem okazało się utrzymanie klasy razem i kontrolowanie jej za pomocą funkcji szpiegowskich. Pracował bezbłędnie przez prawie rok i mógł z łatwością wytrzymać zmiany modeli. Od tego czasu nie używałem tego wzoru, po prostu dobrze wspomniałem, że w niektórych przypadkach jest on wykonalny.
allprog
@allprog „logicznie złożony” - jeśli jest złożony, potrzebujesz złożonych testów. Nie można tego obejść. Szpiedzy sprawią, że będzie to dla ciebie trudniejsze i bardziej złożone. Powinieneś tworzyć zrozumiałe podfunkcje, które możesz testować samodzielnie, zamiast używać szpiegów do testowania ich specjalnego zachowania w innej funkcji.
BT