TDD: Wyśmiewanie ciasno powiązanych obiektów

10

Czasami przedmioty muszą być ściśle połączone. Na przykład CsvFileklasa prawdopodobnie będzie musiała ściśle współpracować z CsvRecordklasą (lub ICsvRecordinterfejsem).

Jednak z tego, czego nauczyłem się w przeszłości, jedną z głównych zasad rozwoju opartego na testach jest: „Nigdy nie testuj więcej niż jednej klasy na raz”. Oznacza to, że powinieneś używać ICsvRecordpróbnych lub kodów pośredniczących zamiast rzeczywistych instancji CsvRecord.

Jednak po wypróbowaniu tego podejścia zauważyłem, że wyśmiewanie się z CsvRecordklasy może być trochę owłosione. Co prowadzi mnie do jednego z dwóch wniosków:

  1. Trudno jest pisać testy jednostkowe! To zapach kodu! Refaktoryzacja!
  2. Wyśmiewanie każdej zależności jest po prostu nieuzasadnione.

Kiedy zastąpiłem moje kpiny rzeczywistymi CsvRecordinstancjami, sprawy potoczyły się znacznie sprawniej. Rozglądając się za myślami innych ludzi, natknąłem się na ten post na blogu , który wydaje się popierać nr 2 powyżej. W przypadku obiektów, które są naturalnie ściśle powiązane, nie powinniśmy się tak bardzo martwić o wyśmiewanie.

Czy jestem na dobrej drodze? Czy są jakieś wady powyższego założenia nr 2? Czy powinienem naprawdę myśleć o refaktoryzacji mojego projektu?

Phil
źródło
1
Myślę, że powszechne jest błędne przekonanie, że „jednostka” w „testach jednostkowych” musi koniecznie być jedną klasą. Myślę, że twój przykład pokazuje przypadek, w którym lepiej byłoby, gdyby te dwie klasy tworzyły jedną jednostkę. Ale nie zrozum mnie źle, całkowicie zgadzam się z odpowiedzią Roberta Harveya.
Doc Brown,

Odpowiedzi:

11

Jeśli naprawdę potrzebujesz koordynacji między tymi dwiema klasami, napisz CsvCoordinatorklasę, która zawiera dwie klasy i przetestuj to.

Kwestionuję jednak pojęcie, które CsvRecordnie może być niezależnie przetestowane. CsvRecordjest w zasadzie klasą DTO , prawda? To tylko zbiór pól z kilkoma metodami pomocniczymi. I CsvRecordmoże być używany także w innych kontekstach CsvFile; możesz na przykład mieć kolekcję lub tablicę CsvRecords.

CsvRecordNajpierw przetestuj . Upewnij się, że pomyślnie przeszedł wszystkie testy. Następnie śmiało i używaj CsvRecordz CsvFileklasą podczas testu. Użyj go jako wstępnie przetestowanego kodu pośredniczącego / próbnego; wypełnij je odpowiednimi danymi testowymi, przekaż je CsvFilei napisz na nim swoje przypadki testowe.

Robert Harvey
źródło
1
Tak, CsvRecord jest zdecydowanie niezależnie testowalny. Problem polega na tym, że jeśli coś psuje się w CsvRecord, spowoduje to niepowodzenie testów CsvData. Ale nie sądzę, że to poważny problem.
Phil
1
Myślę, że chcesz, żeby tak się stało. :)
Robert Harvey,
1
@RobertHarvey: teoretycznie może stać się problemem, jeśli CsvRecord i CsvFile stają się dość złożonymi klasami, a jeśli test psuje się dla CsvFile, teraz nie wiadomo od razu, czy jest to problem w CsvFile lub CsvRecord. Ale wydaje mi się, że jest to bardziej hipotetyczny przypadek - gdybym miał za zadanie zaprogramować takie klasy dla programu w świecie rzeczywistym, zrobiłbym to dokładnie tak, jak to opisujesz.
Doc Brown
2
@Phil: Jeśli się CsvRecordzepsuje, to oczywiście się CsvDatanie powiedzie; ale to jest OK, ponieważ najpierw testujesz CsvRecord, a jeśli to się nie powiedzie, twoje CsvFiletesty są bez znaczenia. Nadal można rozróżnić błędy w CsvRecordi w CsvFile.
tdammers
5

Powodem testowania jednej klasy na raz jest to, że nie chcesz, aby testy dla jednej klasy były zależne od zachowania drugiej klasy. Oznacza to, że jeśli twój test na klasę A ćwiczy dowolną funkcjonalność klasy B, powinieneś wyśmiewać klasę B, aby usunąć zależność od określonej funkcjonalności w klasie B.

CsvRecordWydaje mi się, że klasa taka służy głównie do przechowywania danych - nie jest to klasa o zbyt dużej funkcjonalności. Oznacza to, że może mieć konstruktorów, funkcje pobierające, ustawiające, ale nie ma metod z prawdziwą logiką. Oczywiście zgaduję tutaj - może napisałeś klasę o nazwie, CsvRecordktóra wykonuje wiele skomplikowanych obliczeń.

Ale jeśli CsvRecordnie ma własnej logiki, nie można nic zyskać, kpiąc z niej. To tak naprawdę tylko stara maksyma - „nie kpij z obiektów o wartości” .

Rozważając więc, czy wyśmiewać daną klasę (na test innej klasy), powinieneś wziąć pod uwagę, ile jej własnej logiki ma ta klasa i ile tej logiki zostanie wykonane w trakcie testu.

Dawood ibn Kareem
źródło
+1. Każdy test, którego wynik zależy od poprawności zachowania więcej niż jednego obiektu, jest testem integracyjnym, a nie testem jednostkowym. Musisz wykpić jeden z tych obiektów, aby uzyskać prawdziwy test jednostkowy. Nie dotyczy to jednak obiektów, które nie mają w sobie żadnego rzeczywistego zachowania - na przykład tylko pobierające i ustawiające.
guillaume31,
1

Nie. 2 jest w porządku. Rzeczy mogą być i powinny być ściśle powiązane, jeśli ich koncepcje są ściśle powiązane. Powinno to być rzadkie i na ogół unikać, ale w podanym przykładzie ma to sens.

Telastyn
źródło
0

Klasy „sprzężone” są od siebie wzajemnie zależne. Nie powinno tak być w tym, co opisujesz - CsvRecord nie powinien tak naprawdę dbać o to, że plik CsvFile go zawiera, więc zależność przebiega tylko w jedną stronę. To dobrze i nie jest ciasne połączenie.

W końcu, jeśli klasa zawiera zmienną String, nie twierdziłbyś, że jest ściśle sprzężona z String, prawda?

Więc przetestuj CsvRecord pod kątem pożądanego zachowania.

Następnie użyj kpiny (Mockito jest świetny), aby sprawdzić, czy twoja jednostka wchodzi w interakcje z obiektami, od których zależy poprawnie. Naprawdę zachowanie, które chcesz przetestować - to, że CsvFile obsługuje CsvRcords w oczekiwany sposób. Wewnętrzne funkcjonowanie CvsRecord nie powinno mieć znaczenia - tak właśnie komunikuje się z nim CvsFile.

Wreszcie TDD to nie tylko testy jednostkowe. Z pewnością możesz (i powinieneś) zacząć od testów funkcjonalnych, które sprawdzają funkcjonalne zachowanie się większych komponentów - czyli historii użytkownika lub scenariusza. Twoje testy jednostkowe ustalają oczekiwania i weryfikują elementy, testy funkcjonalne robią to samo dla całości.

Matthew Flynn
źródło
1
-1, ścisłe sprzężenie niekoniecznie oznacza cykliczne zależności, to błędne przekonanie. W tym przykładzie CsvFile jest ściśle powiązany CsvRecord(ale nie na odwrót). PO pyta, czy jest to dobry pomysł do testu CsvFileprzez odłączenie go od CsvRecordpośrednictwem ICsvRecord, a nie odwrotnie.
Doc Brown
2
@DocBrown: To, czy sprzężenie jest ścisłe, CsvFilezależy od tego, jak bardzo zależy od wewnętrznego działania CsvRecord, to znaczy od liczby założeń pliku dotyczących rekordu. Interfejsy pomagają dokumentować i egzekwować takie założenia (a raczej brak innych założeń), ale ilość sprzężeń pozostaje taka sama, z tym wyjątkiem, że za pomocą interfejsu można podłączyć inną klasę rekordów CsvFile. Wprowadzenie interfejsu tylko po to, by powiedzieć, że masz zmniejszone sprzężenie, jest głupie.
tdammers,
0

Są tutaj naprawdę dwa pytania. Pierwszy dotyczy sytuacji, w których drwiny z obiektu są niewskazane. To niewątpliwie prawda, jak pokazują inne doskonałe odpowiedzi. Drugie pytanie dotyczy tego, czy konkretny przypadek jest jedną z takich sytuacji. W tej kwestii nie jestem przekonany.

Prawdopodobnie najczęstszym powodem, aby nie kpić z klasy, jest to, że jest to klasa wartości. Musisz jednak spojrzeć na przyczynę tej reguły. Nie dlatego, że wyśmiewana klasa będzie jakoś zła, ale dlatego, że będzie zasadniczo identyczna z oryginałem. Gdyby tak było, testowanie jednostek nie byłoby łatwiejsze przy użyciu oryginalnej klasy.

Może się zdarzyć, że Twój kod jest jednym z rzadkich wyjątków, w których refaktoryzacja nie pomogłaby, ale powinieneś go zadeklarować tylko wtedy, gdy staranne refaktoryzacje nie zadziałały. Nawet doświadczeni programiści mogą mieć problemy ze znalezieniem alternatyw dla własnego projektu. Jeśli nie możesz wymyślić żadnego możliwego sposobu na jego ulepszenie, poproś kogoś z doświadczeniem, aby dał mu drugie spojrzenie.

Wydaje się, że większość ludzi zakłada, że ​​Twoja CsvRecordklasa jest wartością. Spróbuj to zrobić. Niech to będzie niezmienne, jeśli możesz. Jeśli masz dwa obiekty ze wskaźnikami do siebie, usuń jeden z nich i wymyśl, jak to zrobić. Poszukaj miejsc do podziału klas i funkcji. Najlepszym miejscem do podziału klasy nie zawsze jest dopasowanie do fizycznego układu pliku. Spróbuj odwrócić relacje rodzic / dziecko klas. Być może potrzebujesz osobnej klasy do odczytu i zapisu plików csv. Być może potrzebujesz osobnych klas do obsługi pliku I / O i interfejsu do wyższych warstw. Jest wiele rzeczy, które należy wypróbować, zanim uznamy, że jest to bezrefrakcyjne.

Karl Bielefeldt
źródło