Większość projektów, nad którymi pracuję, rozważa rozwój i testy jednostkowe w oderwaniu, co sprawia, że pisanie testów jednostkowych w późniejszym przypadku jest koszmarem. Moim celem jest pamiętanie o testach podczas samych faz projektowania wysokiego i niskiego poziomu.
Chcę wiedzieć, czy istnieją dobrze zdefiniowane zasady projektowania, które promują testowalny kod. Jedną z takich zasad, którą ostatnio zrozumiałem, jest Inwersja zależności poprzez wstrzyknięcie zależności i Inwersja kontroli.
Czytałem, że istnieje coś takiego jak SOLID. Chcę zrozumieć, czy przestrzeganie zasad SOLID pośrednio prowadzi do kodu, który można łatwo przetestować? Jeśli nie, to czy istnieją dobrze zdefiniowane zasady projektowania, które promują testowalny kod?
Wiem, że istnieje coś takiego jak Test Driven Development. Chociaż bardziej interesuje mnie projektowanie kodu z myślą o testowaniu podczas samej fazy projektowania, niż kierowanie projektem przez testy. Mam nadzieję, że to ma sens.
Jeszcze jedno pytanie związane z tym tematem brzmi: czy w porządku jest ponowne uwzględnienie istniejącego produktu / projektu i wprowadzenie zmian w kodzie i projekcie w celu umożliwienia napisania jednostkowego przypadku testowego dla każdego modułu?
źródło
Odpowiedzi:
Tak, SOLID to bardzo dobry sposób na zaprojektowanie kodu, który można łatwo przetestować. Jako krótki podkład:
S - Zasada pojedynczej odpowiedzialności: Obiekt powinien zrobić dokładnie jedną rzecz i powinien być jedynym obiektem w bazie kodu, który wykonuje tę jedną rzecz. Weźmy na przykład klasę domeny, powiedzmy fakturę. Klasa Faktura powinna reprezentować strukturę danych i reguły biznesowe faktury stosowanej w systemie. Powinna to być jedyna klasa reprezentująca fakturę w bazie kodu. Można to dalej rozbić, aby powiedzieć, że metoda powinna mieć jeden cel i powinna być jedyną metodą w bazie danych, która spełnia tę potrzebę.
Postępując zgodnie z tą zasadą, zwiększasz testowalność swojego projektu, zmniejszając liczbę testów, które musisz napisać, które testują tę samą funkcjonalność na różnych obiektach, a także zazwyczaj uzyskujesz mniejsze części funkcjonalności, które są łatwiejsze do przetestowania w izolacji.
O - Zasada otwarta / zamknięta: klasa powinna być otwarta na rozszerzenie, ale zamknięta na zmianę . Gdy obiekt istnieje i działa poprawnie, idealnie nie powinno być potrzeby powrotu do tego obiektu, aby wprowadzić zmiany, które dodają nową funkcjonalność. Zamiast tego obiekt należy rozszerzyć, albo poprzez jego wyprowadzenie, albo przez podłączenie do niego nowych lub różnych implementacji zależności, aby zapewnić tę nową funkcjonalność. Pozwala to uniknąć regresji; możesz wprowadzić nową funkcjonalność tam, gdzie jest potrzebna, bez zmiany zachowania obiektu, ponieważ jest on już używany gdzie indziej.
Przestrzegając tej zasady, ogólnie zwiększasz zdolność kodu do tolerowania „próbnych”, a także unikasz przepisywania testów w celu przewidywania nowego zachowania; wszystkie istniejące testy dla obiektu powinny nadal działać w przypadku nierozszerzonej implementacji, a także powinny działać nowe testy dla nowej funkcjonalności korzystającej z rozszerzonej implementacji.
L - Zasada podstawienia Liskowa: Klasa A, zależna od klasy B, powinna mieć możliwość używania dowolnego X: B bez znajomości różnicy. Zasadniczo oznacza to, że wszystko, czego użyjesz jako zależność, powinno mieć podobne zachowanie, jakie widzi klasa zależna. Jako krótki przykład powiedzmy, że masz interfejs IWriter, który udostępnia zapis (ciąg), który jest implementowany przez ConsoleWriter. Teraz musisz napisać do pliku, więc utworzysz FileWriter. Czyniąc to, musisz upewnić się, że FileWriter może być używany w taki sam sposób, jak ConsoleWriter (co oznacza, że jedyny sposób, w jaki osoba zależna może z nim współdziałać, wywołując Write (string)), a także dodatkowe informacje, które może wymagać FileWriter zadanie (takie jak ścieżka i plik do zapisu) musi być dostarczone skądinąd poza zależną.
Jest to ogromna zaleta przy pisaniu testowalnego kodu, ponieważ projekt zgodny z LSP może w dowolnym momencie zastąpić rzeczywisty obiekt „kpionym” obiektem bez zmiany oczekiwanego zachowania, umożliwiając testowanie małych fragmentów kodu w sposób pewny że system będzie wtedy pracował z podłączonymi prawdziwymi obiektami.
I - Zasada segregacji interfejsu: Interfejs powinien mieć tak mało metod, jak to możliwe, aby zapewnić funkcjonalność roli zdefiniowanej przez interfejs . Krótko mówiąc, więcej mniejszych interfejsów jest lepszych niż mniejszych interfejsów. Wynika to z faktu, że duży interfejs ma więcej powodów do zmiany i powoduje więcej zmian w innym miejscu w bazie kodu, które mogą nie być konieczne.
Zgodność z ISP poprawia testowalność poprzez zmniejszenie złożoności testowanych systemów i zależności tych SUT. Jeśli testowany obiekt zależy od interfejsu IDoThreeThings, który udostępnia DoOne (), DoTwo () i DoThree (), musisz wyśmiewać obiekt, który implementuje wszystkie trzy metody, nawet jeśli obiekt używa tylko metody DoTwo. Ale jeśli obiekt zależy tylko od IDoTwo (który udostępnia tylko DoTwo), możesz łatwiej wyśmiewać obiekt, który ma tę jedną metodę.
D - Zasada odwrócenia zależności: konkrecje i abstrakcje nigdy nie powinny zależeć od innych konkrecji, ale od abstrakcji . Ta zasada bezpośrednio egzekwuje zasadę luźnego sprzężenia. Obiekt nigdy nie powinien wiedzieć, czym JEST obiekt; zamiast tego powinno obchodzić się, co robi obiekt. Dlatego użycie interfejsów i / lub abstrakcyjnych klas bazowych zawsze powinno być preferowane w porównaniu z zastosowaniem konkretnych implementacji podczas definiowania właściwości i parametrów obiektu lub metody. Pozwala to na zamianę jednej implementacji na inną bez konieczności zmiany użycia (jeśli podążasz za LSP, co idzie w parze z DIP).
Ponownie, ma to ogromne znaczenie dla testowalności, ponieważ pozwala raz jeszcze wstrzyknąć próbną implementację zależności zamiast implementacji „produkcyjnej” do testowanego obiektu, jednocześnie testując obiekt w dokładnie takiej formie, jaką będzie miał podczas w produkcji. Jest to klucz do testów jednostkowych „w izolacji”.
źródło
Jeśli zastosowane poprawnie, tak. Jest post na blogu Jeffa wyjaśniający zasady SOLID w naprawdę krótkim czasie (wspomniany podcast też jest wart wysłuchania), proponuję zajrzeć tam, jeśli dłuższe opisy cię wyrzucają.
Z mojego doświadczenia, 2 zasady SOLID odgrywają główną rolę w projektowaniu testowalnego kodu:
Wierzę, że te dwa najbardziej pomogą ci przy projektowaniu pod kątem testowalności. Pozostałe mają również wpływ, ale powiedziałbym, że nie są tak duże.
Bez istniejących testów jednostkowych jest to po prostu proste - proszenie o problemy. Testów jednostkowych jest gwarancją, że kod działa . Wprowadzenie przełomowej zmiany jest zauważane natychmiast, jeśli masz odpowiedni zasięg testów.
Teraz, jeśli chcesz zmienić istniejący kod w celu dodania testów jednostkowych , wprowadza to lukę, w której nie masz jeszcze testów, ale zmieniłeś już kod . Oczywiście możesz nie mieć pojęcia, co złamały twoje zmiany. Jest to sytuacja, której chcesz uniknąć.
Testy jednostkowe i tak warto pisać, nawet w przypadku kodu trudnego do przetestowania. Jeśli kod działa , ale nie jest testowany jednostkowo, właściwym rozwiązaniem byłoby napisanie testów, a następnie wprowadzenie zmian. Należy jednak pamiętać, że zmiana przetestowanego kodu w celu ułatwienia jego testowania jest czymś, na czym kierownictwo może nie chcieć wydawać pieniędzy (prawdopodobnie usłyszysz, że nie przynosi żadnej wartości biznesowej).
źródło
TWOJE PIERWSZE PYTANIE:
SOLID jest rzeczywiście właściwą drogą. Uważam, że dwa najważniejsze aspekty akronimu SOLID, jeśli chodzi o testowalność, to S (Single Responsibility) i D (Dependency Injection).
Jedna odpowiedzialność : Twoje zajęcia powinny naprawdę robić tylko jedną rzecz i tylko jedną rzecz. klasa, która tworzy plik, analizuje dane wejściowe i zapisuje je w pliku, robi już trzy rzeczy. Jeśli twoja klasa robi tylko jedną rzecz, wiesz dokładnie, czego się po niej spodziewać, a projektowanie przypadków testowych powinno być dość łatwe.
Dependency Injection (DI): Daje ci kontrolę nad środowiskiem testowym. Zamiast tworzyć obce obiekty w kodzie, wstrzykujesz go za pomocą konstruktora klasy lub wywołania metody. Kiedy nie poddajesz się dyskusji, po prostu zamieniasz prawdziwe klasy na kody pośredniczące lub kpiny, które całkowicie kontrolujesz.
TWOJE DRUGIE PYTANIE: Idealnie, piszesz testy dokumentujące działanie twojego kodu przed jego refaktoryzacją. W ten sposób możesz udokumentować, że refaktoryzacja odtwarza te same wyniki, co oryginalny kod. Problemem jest jednak to, że działający kod jest trudny do przetestowania. To klasyczna sytuacja! Moja rada to: Przed testowaniem jednostkowym dokładnie przemyśl fakturę. Jeśli możesz; napisz testy dla działającego kodu, następnie refaktoryzuj kod, a następnie refaktoryzuj testy. Wiem, że będzie to kosztować godziny, ale będziesz bardziej pewny, że refaktoryzowany kod działa tak samo jak stary. Powiedziawszy to, poddałem się wiele razy. Klasy mogą być tak brzydkie i nieporządne, że przepisywanie jest jedynym sposobem, aby umożliwić ich testowanie.
źródło
Oprócz innych odpowiedzi, które koncentrują się na uzyskaniu luźnego sprzężenia, chciałbym powiedzieć słowo o testowaniu skomplikowanej logiki.
Kiedyś musiałem przetestować jednostkę, której logika była skomplikowana, z wieloma warunkami warunkowymi i gdzie trudno było zrozumieć rolę pól.
Zamieniłem ten kod na wiele małych klas reprezentujących maszynę stanu . Logika stała się znacznie prostsza, ponieważ różne stany poprzedniej klasy stały się wyraźne. Każda klasa stanowa była niezależna od innych, dlatego można je było łatwo przetestować.
Fakt, że stany były jawne, ułatwił wyliczenie wszystkich możliwych ścieżek kodu (przejścia stanów), a tym samym napisanie testu jednostkowego dla każdego z nich.
Oczywiście nie każdą złożoną logikę można modelować jako maszynę stanu.
źródło
SOLID to doskonały początek, z mojego doświadczenia wynika, że cztery aspekty SOLID naprawdę dobrze działają z testami jednostkowymi.
Chciałbym również przyjrzeć się różnym wzorom, zwłaszcza wzorcom fabrycznym. Załóżmy, że masz konkretną klasę, która implementuje interfejs. Utworzyłbyś fabrykę, aby utworzyć instancję konkretnej klasy, ale zamiast tego zwraca interfejs.
W swoich testach możesz użyć Moq lub innej frakcji próbnej, aby zastąpić tę wirtualną metodę i zwrócić interfejs swojego projektu. Ale jeśli chodzi o kod wykonawczy, fabryka się nie zmieniła. W ten sposób możesz również ukryć wiele szczegółów implementacji, kod implementacyjny nie dba o to, jak zbudowany jest interfejs, liczy się tylko odzyskanie interfejsu.
Jeśli chcesz nieco rozwinąć tę kwestię, zdecydowanie polecam przeczytanie The Art of Unit Testing . Daje kilka świetnych przykładów korzystania z tych zasad i jest dość szybki.
źródło