Jednostka testująca klasę, która wykorzystuje DI bez testowania elementów wewnętrznych

12

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 IPersonRepositoryi IEmailRepositoryzastrzyk, 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 IPersonRepositoryi 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.GetSavingsByCustomerIdzwraca 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ć.

Michel
źródło
1
Gdy tylko twoja główna klasa oczekuje IPersonRepositoryobiektu, 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).
Doc Brown,

Odpowiedzi:

7

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.GetSavingsByCustomerIdzwraca x. Ale potem mój test jednostkowy „wie” o wewnętrznym działaniu, ponieważ „wie”, które metody wyśmiewać, a które nie.

Masz rację, że jest to naruszenie zasady „nie testuj elementów wewnętrznych” i jest to powszechne zjawisko, które ludzie przeoczają.

Jak mogę przetestować klasę, która wstrzyknęła zależności, bez testowania wiedzy o elementach wewnętrznych?

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 metod IPersonRepository, 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ć GetSavingsByCustomerIdmetody, 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.

David Arno
źródło
1
Bardzo podoba mi się pomysł nr 2. Nie wiem, dlaczego wcześniej o tym nie myślałem. Zależności od IReposositów tworzę od kilku lat, nie zastanawiając się, dlaczego stworzyłem zależność od całego IRepository (który w wielu przypadkach będzie zawierał sporo sygnatur metod), podczas gdy ma on zależność tylko od jednego lub 2 metody. Więc zamiast wstrzykiwać IRepository, mógłbym lepiej wstrzyknąć lub przekazać wymaganą (jedyną) sygnaturę metody.
Michel
1

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

Ewan
źródło
2
To wciąż prowadzi do sytuacji, w której tworzę próbkę z danymi dokładnie dla metod, z którymi pracuje testowana klasa. Nadal mam problem, że po zmianie implementacji test się zepsuje.
Michel
1
zredagowano, aby wyjaśnić, dlaczego moje podejście jest lepsze, choć myślę, że nadal nie jest to idealne rozwiązanie w scenariuszu
Ewan
1

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)

Andy
źródło