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.
źródło
Odpowiedzi:
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.
źródło
Stream
klasa, ale nie ma ścisłego powiązania.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.
źródło
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”.
źródło
Odpowiem w dwóch częściach:
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.
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ę.
źródło
Widziałem wiele korzyści, odkąd zacząłem programować bardziej jako interfejs poza polimorfizmem.
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.
źródło