Mam klasę, która jest refaktoryzowana w 1 klasie głównej i 2 mniejszych klasach. Główne klasy korzystają z bazy danych (jak wiele moich klas) i wysyła wiadomość e-mail. Tak więc główna klasa ma IPersonRepository
i IEmailRepository
zastrzyk, który z kolei wysyła do 2 mniejszych klas.
Teraz chcę przetestować jednostkę w klasie głównej i nauczyłem się nie unittestować wewnętrznych działań klasy, ponieważ powinniśmy być w stanie zmienić wewnętrzne funkcjonowanie bez przerywania testów jednostkowych.
Ale ponieważ klasa używa IPersonRepository
i IEmailRepository
, MUSZĘ podać (mock / dummy) wyniki dla niektórych metod dla IPersonRepository
. Główna klasa oblicza niektóre dane na podstawie istniejących danych i zwraca je. Jeśli chcę to przetestować, nie widzę, jak mogę napisać test bez określenia, że IPersonRepository.GetSavingsByCustomerId
zwraca x. Ale potem mój test jednostkowy „wie” o wewnętrznym działaniu, ponieważ „wie”, które metody wyśmiewać, a które nie.
Jak mogę przetestować klasę, która wstrzyknęła zależności, bez testowania wiedzy o elementach wewnętrznych?
tło:
Z mojego doświadczenia wynika, że wiele takich testów tworzy makiety dla repozytoriów, a następnie albo dostarcza odpowiednie dane dla makiet, albo testuje, czy podczas wykonywania została wywołana określona metoda. Tak czy inaczej, test wie o elementach wewnętrznych.
Teraz widziałem prezentację na temat teorii (którą wcześniej słyszałem), że test nie powinien wiedzieć o implementacji. Po pierwsze dlatego, że nie testujesz, jak to działa, ale także dlatego, że kiedy teraz zmienisz implementację, wszystkie testy jednostkowe kończą się niepowodzeniem, ponieważ „wiedzą” o implementacji. Chociaż podoba mi się koncepcja testów nieświadomych implementacji, nie wiem, jak to zrobić.
źródło
IPersonRepository
obiektu, interfejs i wszystkie metody, które opisuje, nie są już „wewnętrzne”, więc nie jest to tak naprawdę problemem testu. Wasze prawdziwe pytanie powinno brzmieć: „jak mogę przefakturować klasy na mniejsze jednostki, nie ujawniając zbyt wiele publicznie”. Odpowiedź brzmi „utrzymuj interfejsy w niskiej jakości” (na przykład przestrzegając zasady segregacji interfejsów). To jest punkt 2 IMHO w odpowiedzi @ DavidArno (chyba nie muszę powtarzać tego w innej odpowiedzi).Odpowiedzi:
Masz rację, że jest to naruszenie zasady „nie testuj elementów wewnętrznych” i jest to powszechne zjawisko, które ludzie przeoczają.
Istnieją dwa rozwiązania, które można zastosować w celu obejścia tego naruszenia:
1) Podaj pełną próbkę
IPersonRepository
. Twoje obecnie opisane podejście polega na powiązaniu kpiny z wewnętrznym działaniem testowanej metody poprzez kpienie z metod, które wywoła. Jeśli dostarczysz makiety dla wszystkich metodIPersonRepository
, usuniesz to połączenie. Wewnętrzne funkcjonowanie może się zmieniać bez wpływu na próbę, dzięki czemu test jest mniej kruchy.Zaletą tego podejścia jest uproszczenie mechanizmu DI, ale może on spowodować wiele pracy, jeśli interfejs definiuje wiele metod.
2) Nie wstrzykiwać
IPersonRepository
, ani wstrzykiwaćGetSavingsByCustomerId
metody, ani wstrzykiwać wartości oszczędności. Problem z wstrzykiwaniem całych implementacji interfejsu polega na tym, że wstrzykujesz („mów, nie pytaj”) system „pytaj, nie mów”, mieszając oba podejścia. Jeśli zastosujesz podejście „czysto DI”, metoda powinna zostać podana (powiedzona) dokładnie, jaką metodę ma wywołać, jeśli chce uzyskać wartość oszczędności, zamiast otrzymać obiekt (który następnie musi efektywnie poprosić o metodę wywołania).Zaletą tego podejścia jest to, że pozwala uniknąć próbnych prób (poza metodami testowymi, które wstrzykujesz do testowanej metody). Wadą jest to, że może powodować zmianę sygnatur metod w odpowiedzi na wymagania zmiany metody.
Oba podejścia mają swoje zalety i wady, więc wybierz ten, który najlepiej odpowiada Twoim potrzebom.
źródło
Moje podejście polega na stworzeniu „próbnych” wersji repozytoriów, które czytają z prostych plików zawierających potrzebne dane.
Oznacza to, że indywidualny test nie „wie” o konfiguracji próbnej, chociaż oczywiście cały projekt testowy będzie odwoływał się do próbnej i zawierał pliki instalacyjne itp.
Pozwala to uniknąć złożonej konfiguracji próbnego obiektu wymaganej przez fałszywe frameworki i pozwala używać „próbnych” obiektów w rzeczywistych instancjach aplikacji do testowania interfejsu użytkownika i tym podobnych.
Ponieważ „makieta” jest w pełni zaimplementowana, a nie specjalnie skonfigurowana w scenariuszu testowym, zmiana implementacji, na przykład pozwala powiedzieć, że GetSavingsForCustomer powinien teraz również usunąć klienta. Nie zepsuje testów (chyba że tak naprawdę zepsuje testy), potrzebujesz tylko zaktualizować swoją pojedynczą próbną implementację, a wszystkie testy będą działać przeciwko niej bez zmiany ich konfiguracji
źródło
Testy jednostkowe to zwykle testy whitebox (masz dostęp do prawdziwego kodu). Dlatego dobrze jest znać elementy wewnętrzne do pewnego stopnia, jednak dla początkujących łatwiej jest tego nie robić, ponieważ nie należy testować zachowania wewnętrznego (takiego jak „wywoływanie metody najpierw, potem b, a potem jeszcze raz”).
Wstrzyknięcie makiety dostarczającej dane jest w porządku, ponieważ twoja klasa (jednostka) zależy od danych zewnętrznych (lub dostawcy danych). Powinieneś jednak zweryfikować wyniki (nie sposób, aby się do nich dostać)! np. podajesz wystąpienie osoby i weryfikujesz, czy wiadomość e-mail została wysłana na poprawny adres e-mail, np. poprzez podanie próbnej wiadomości e-mail, że próbka nie przechowuje nic poza przechowywaniem adresu e-mail adresata w celu późniejszego dostępu przez test -kod. (Myślę, że Martin Fowler nazywa je raczej Stubami niż Mockami)
źródło