Pisanie testowalnego kodu a unikanie spekulatywności


11

Czytałem dziś rano kilka postów na blogu i natknąłem się na ten :

Jeśli jedyną klasą, która kiedykolwiek implementuje interfejs klienta, jest CustomerImpl, tak naprawdę nie ma polimorfizmu i zastępowalności, ponieważ w praktyce nie ma nic do zastąpienia w czasie wykonywania. To fałszywa ogólność.

Ma to dla mnie sens, ponieważ implementacja interfejsu zwiększa złożoność, a jeśli jest tylko jedna implementacja, można argumentować, że powoduje ona niepotrzebną złożoność. Pisanie kodu, który jest bardziej abstrakcyjny, niż powinien być, jest często uważany za zapach kodu zwany „spekulatywnością” (wspomniany również w poście).

Ale jeśli postępuję zgodnie z TDD, nie mogę (łatwo) tworzyć podwójnych testów bez tej spekulatywnej ogólności, czy to w postaci implementacji interfejsu, czy naszej innej opcji polimorficznej, dzięki czemu klasa jest dziedziczna, a jej metody wirtualne.

Jak więc pogodzić ten kompromis? Czy warto spekulować ogólnie, aby ułatwić testowanie / TDD? Jeśli używasz podwójnych testów, czy są one liczone jako drugie implementacje, a tym samym nie powodują już spekulacji? Czy powinieneś rozważyć bardziej rygorystyczne ramy kpiny, które pozwalają kpić z konkretnych współpracowników (np. Moles kontra Moq w świecie C #)? A może powinieneś przetestować konkretne klasy i napisać coś, co można by uznać za testy „integracyjne”, dopóki projekt nie będzie naturalnie wymagał polimorfizmu?

Z ciekawością czytam opinie innych osób na ten temat - z góry dziękuję za opinie.


Osobiście uważam, że byty nie powinny być wyśmiewane. Szydzę tylko z usług, a one w każdym razie potrzebują interfejsu, ponieważ kod domeny kodowej zazwyczaj nie ma odniesienia do kodu, w którym usługi są implementowane.
CodesInChaos

7
My, użytkownicy języków dynamicznie wpisywanych, śmiejimy się z tego, że się wkurzysz z łańcuchów, które nałożyły na ciebie języki, w których występują pisma statyczne. Jest to jedna rzecz, która ułatwia testowanie jednostek w dynamicznie pisanych językach. Nie muszę opracowywać interfejsu, aby zastąpić obiekt do celów testowych.
Winston Ewert

Interfejsy służą nie tylko do generalizacji. Są one wykorzystywane do wielu celów, odsprzęganie kodu jest jednym z najważniejszych. To z kolei znacznie ułatwia testowanie kodu.
Marjan Venema

@WinstonEwert To interesująca korzyść z dynamicznego pisania, której wcześniej nie brałem pod uwagę jako kogoś, kto, jak zauważyłeś, zwykle nie działa w dynamicznie pisanych językach.
Erik Dietrich,

@CodeInChaos Nie rozważyłem rozróżnienia dla celów tego pytania, chociaż jest to rozsądne rozróżnienie. W takim przypadku możemy pomyśleć o klasach usług / ram z tylko jedną (bieżącą) implementacją. Powiedzmy, że mam bazę danych, do której mam dostęp dzięki DAO. Czy nie powinienem interfejsować DAO, dopóki nie będę mieć wtórnego modelu trwałości? (Wydaje się, że sugeruje to autor postu na blogu)
Erik Dietrich,

Odpowiedzi:


14

Poszedłem i przeczytałem post na blogu i zgadzam się z wieloma wypowiedziami autora. Jeśli jednak piszesz kod przy użyciu interfejsów do celów testowania jednostkowego, powiedziałbym, że próbna implementacja interfejsu jest twoją drugą implementacją. Twierdziłbym, że tak naprawdę nie dodaje zbyt wiele złożoności do twojego kodu, szczególnie jeśli kompromis nie zrobienia tego powoduje, że twoje klasy są ściśle powiązane i trudne do przetestowania.


3
Dokładnie tak. Kod testowy jest częścią aplikacji, ponieważ potrzebujesz go do prawidłowego zaprojektowania, wdrożenia, konserwacji itp. Fakt, że nie wysyłasz go do klienta, jest nieistotny - jeśli w twoim pakiecie testowym jest druga implementacja, ogólność jest tam i powinieneś ją zaakceptować.
Kilian Foth,

1
Jest to odpowiedź, która wydaje mi się najbardziej przekonująca (i @KilianFoth dodaje, że niezależnie od tego, czy kod jest wysyłany, druga implementacja nadal istnieje). Będę jednak wstrzymywał się z przyjęciem odpowiedzi, aby sprawdzić, czy ktoś jeszcze się w nią włączy.
Erik Dietrich,

Dodałbym również, że jeśli zależysz od interfejsów w testach, ogólność nie jest już spekulatywna
Pete

„Nie robienie tego” (przy użyciu interfejsów) nie powoduje automatycznie ścisłego powiązania klas. Po prostu nie. Np. W .NET Framework istnieje Streamklasa, ale nie ma ścisłego powiązania.
Luke Puplett,

3

Testowanie kodu ogólnie nie jest łatwe. Gdyby tak było, robilibyśmy to już dawno temu i nie robilibyśmy tego w ciągu ostatnich 10-15 lat. Jedną z największych trudności zawsze było określenie, w jaki sposób przetestować kod, który został napisany w sposób spójny, dobrze przemyślany i możliwy do przetestowania bez przerywania enkapsulacji. Dyrektor BDD sugeruje, że skupiamy się prawie całkowicie na zachowaniu, i pod pewnymi względami wydaje się sugerować, że tak naprawdę nie musisz tak bardzo martwić się wewnętrznymi szczegółami, ale często może to utrudnić sprawdzenie, czy istnieją wiele prywatnych metod, które robią „rzeczy” w bardzo ukryty sposób, ponieważ może to zwiększyć ogólną złożoność testu, aby poradzić sobie ze wszystkimi możliwymi wynikami na poziomie bardziej publicznym.

Kpiny mogą w pewnym stopniu pomóc, ale znowu są dość zewnętrznie skoncentrowane. Wstrzykiwanie zależności może również działać całkiem nieźle, ponownie z próbami lub podwójnymi testami, ale może to również wymagać ujawnienia elementów za pośrednictwem interfejsu lub bezpośrednio, że w innym przypadku wolałbyś pozostać ukryty - jest to szczególnie prawdziwe, jeśli chcesz mieć niezły poziom bezpieczeństwa paranoicznego na temat niektórych klas w twoim systemie.

Dla mnie jury wciąż nie zastanawia się, czy zaprojektować twoje klasy, aby były łatwiejsze do przetestowania. Może to powodować problemy, jeśli okaże się, że musisz dostarczyć nowe testy przy zachowaniu starszego kodu. Zgadzam się, że powinieneś być w stanie przetestować absolutnie wszystko w systemie, ale nie podoba mi się pomysł ujawnienia - nawet pośrednio - prywatnych elementów wewnętrznych klasy, tylko po to, aby napisać dla nich test.

Dla mnie rozwiązaniem zawsze było przyjęcie dość pragmatycznego podejścia i połączenie szeregu technik w celu dopasowania do konkretnej sytuacji. Używam wielu odziedziczonych podwójnych testów, aby ujawnić wewnętrzne właściwości i zachowania dla moich testów. Kpię ze wszystkiego, co można dołączyć do moich klas, a tam, gdzie nie zagrozi to bezpieczeństwu moich klas, zapewnię środki do nadpisania lub wstrzyknięcia zachowań do celów testowania. Rozważę nawet udostępnienie interfejsu bardziej opartego na zdarzeniach, jeśli pomoże to poprawić zdolność do testowania kodu

Tam, gdzie znajduję jakiś „niestabilny” kod, szukam, czy mogę refaktoryzować, aby uczynić rzeczy bardziej testowalnymi. Tam, gdzie masz dużo prywatnego kodu, który ukrywa się za kulisami, często znajdziesz nowe klasy, które czekają na przełamanie. Klasy te mogą być używane wewnętrznie, ale często można je testować niezależnie, zachowując mniej prywatnych zachowań, a następnie często mniej warstw dostępu i złożoności. Jedną rzeczą, której staram się unikać, jest jednak pisanie kodu produkcyjnego z wbudowanym kodem testowym. Może być kuszące tworzenie „ końcówek testowych ”, które skutkują takimi okropnościami if testing then ..., które wskazują na problem z testowaniem, który nie został w pełni zdekonstruowany i nie został w pełni rozwiązany.

Pomocne może okazać się przeczytanie książki xUnit Test Patterns Gerarda Meszarosa , która omawia wszystkie tego rodzaju rzeczy bardziej szczegółowo niż ja. Prawdopodobnie nie robię wszystkiego, co on sugeruje, ale pomaga wyjaśnić niektóre trudniejsze sytuacje testowe. Na koniec dnia chcesz być w stanie spełnić wymagania dotyczące testowania, jednocześnie stosując preferowane projekty, i pomaga to lepiej zrozumieć wszystkie opcje, aby lepiej zdecydować, gdzie możesz pójść na kompromis.


1

Czy używany język ma sposób „kpić” z obiektu do testowania? Jeśli tak, te irytujące interfejsy mogą zniknąć.

Z drugiej strony mogą istnieć powody, by mieć SimpleInterface i jedyną ComplexThing, która go implementuje. Mogą istnieć fragmenty ComplexThing, które nie powinny być dostępne dla użytkownika SimpleInterface. Nie zawsze jest to spowodowane nadmiernie rozkwitającym koderem OO.

Odejdę teraz i pozwolę wszystkim skoczyć na fakt, że kod, który to robi, „pachnie im”.


Tak, pracuję w języku (językach) z kpiącymi ramami, które obsługują kpiące konkretne obiekty. Aby to zrobić, narzędzia te wymagają różnego stopnia przeskakiwania przez obręcze.
Erik Dietrich,

0

Odpowiem w dwóch częściach:

  1. Nie potrzebujesz interfejsów, jeśli jesteś zainteresowany testowaniem. W tym celu używam frameworków (w Javie: Mockito lub easymock). Uważam, że projektowany kod nie powinien być modyfikowany do celów testowych. Pisanie testowalnego kodu jest równoważne pisaniu kodu modułowego, więc zwykle piszę kod modułowy (testowalny) i testuję tylko publiczne interfejsy kodu.

  2. Pracuję w dużym projekcie Java i staję głęboko przekonany, że za pomocą read-only (tylko pobierające) interfejsów jest droga (Należy pamiętać, że jestem wielkim fanem niezmienności). Klasa implementująca może mieć elementy ustawiające, ale jest to szczegół implementacji, który nie powinien być wystawiany na zewnętrzne warstwy. Z innej perspektywy wolę kompozycję niż dziedziczenie (modułowość, pamiętasz?), Więc interfejsy też tu pomagają. Jestem gotów ponieść koszty spekulacyjnej ogólności, niż strzelić sobie w stopę.


0

Widziałem wiele korzyści, odkąd zacząłem programować bardziej jako interfejs poza polimorfizmem.

  • Zmusza mnie do wcześniejszego zastanowienia się nad interfejsem klasy (metodami publicznymi) i tym, jak będzie ona współdziałać z interfejsami innych klas.
  • Pomaga mi pisać mniejsze klasy, które są bardziej spójne i zgodne z jedną zasadą odpowiedzialności.
  • Łatwiej jest przetestować mój kod
  • Mniej klas statycznych / stanu globalnego, ponieważ klasa musi być na poziomie instancji
  • Łatwiejsza integracja i montaż elementów razem, zanim cały program będzie gotowy
  • Wstrzykiwanie zależności, oddzielające konstrukcję obiektu od logiki biznesowej

Wiele osób zgodzi się, że więcej, mniejsze klasy są lepsze niż mniej, większe klasy. Nie musisz skupiać się na jednym czasie, a każda klasa ma jasno określony cel. Inni mogą powiedzieć, że zwiększasz złożoność, mając więcej klas.

Dobrze jest używać narzędzi do zwiększania produktywności, ale myślę, że poleganie wyłącznie na frameworkach Mock, a zamiast budowania testowalności i modułowości bezpośrednio w kodzie na dłuższą metę spowoduje obniżenie jakości kodu.

Podsumowując, uważam, że pomogło mi to napisać kod wyższej jakości, a korzyści znacznie przewyższają wszelkie konsekwencje.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.