Zachowania związane z testowaniem jednostkowym bez powiązania ze szczegółami implementacji

16

W swoim wystąpieniu TDD, gdzie wszystko poszło nie tak , Ian Cooper popiera pierwotną intencję Kenta Becka za testowaniem jednostkowym w TDD (testowanie zachowań, a nie metod klas) i argumentuje za unikaniem łączenia testów z implementacją.

W przypadku zachowania takiego jak save X to some data sourcew systemie z typowym zestawem usług i repozytoriów, w jaki sposób możemy testować jednostkowo zapisywanie niektórych danych na poziomie usługi za pośrednictwem repozytorium, bez powiązania testu ze szczegółami implementacji (np. Wywołanie określonej metody )? Czy unikanie tego rodzaju sprzężenia nie jest w jakiś sposób warte wysiłku / zła?

Andy Hunt
źródło
1
Jeśli chcesz przetestować, czy dane zostały zapisane w repozytorium, wtedy test będzie musiał przejść i sprawdzić repozytorium, aby sprawdzić, czy dane tam są, prawda? A może coś mi brakuje?
Moje pytanie dotyczyło raczej unikania łączenia testów ze szczegółami implementacji, takich jak wywoływanie konkretnej metody w repozytorium, czy też to, co należy zrobić.
Andy Hunt

Odpowiedzi:

8

Twój konkretny przykład to przypadek, w którym zwykle musisz przetestować, sprawdzając, czy dana metoda została wywołana, ponieważ saving X to data sourceoznacza komunikowanie się z zewnętrzną zależnością , więc zachowanie, które musisz przetestować, polega na tym, że komunikacja odbywa się zgodnie z oczekiwaniami .

Nie jest to jednak nic złego. Interfejsy graniczne między aplikacją a jej zewnętrznymi zależnościami nieszczegółami implementacji , w rzeczywistości są zdefiniowane w architekturze systemu; co oznacza, że ​​taka granica najprawdopodobniej się nie zmieni (lub jeśli będzie to konieczne, będzie to najmniej częsta zmiana). Tak więc sprzężenie testów z repositoryinterfejsem nie powinno przysparzać Ci zbyt wiele problemów (jeśli tak, rozważ, czy interfejs nie kradnie obowiązków aplikacji).

Teraz rozważ tylko reguły biznesowe aplikacji oddzielonej od interfejsu użytkownika, baz danych i innych usług zewnętrznych. Jest tak, jeśli musisz mieć swobodę zmiany zarówno struktury, jak i zachowania kodu. W tym miejscu testy sprzęgania i szczegóły implementacji wymuszą zmianę kodu testowego więcej niż kodu produkcyjnego, nawet jeśli nie nastąpi zmiana w ogólnym zachowaniu aplikacji. W tym miejscu testy Statezamiast Interactionpomagać nam iść szybciej.

PS: Nie zamierzam mówić, czy testowanie według stanu lub interakcji jest jedynym prawdziwym sposobem TDD - uważam, że jest to kwestia użycia odpowiedniego narzędzia do właściwej pracy.

MichelHenrich
źródło
Kiedy wspominasz o „komunikowaniu się z zewnętrzną zależnością”, czy masz na myśli zależności zewnętrzne jako te, które są zewnętrzne w stosunku do testowanej jednostki, czy te zewnętrzne dla systemu jako całości?
Andy Hunt
Przez „zewnętrzną zależność” rozumiem wszystko, co można uznać za wtyczkę do aplikacji. Przez aplikację mam na myśli reguły biznesowe, niezależne od wszelkiego rodzaju szczegółów, takich jak ramy, które należy zastosować w przypadku trwałości lub interfejsu użytkownika. Myślę, że wujek Bob może to lepiej wyjaśnić, tak jak w tym
wykładzie
Myślę, że jest to idealne podejście, jak mówi wykład, do testowania na zasadzie „cechy” lub „zachowania” i jednego testu na cechę lub zachowanie (lub permutację jednego, tj. Różnych parametrów). Jednakże, jeśli mam 1 „szczęśliwy” test dla funkcji, aby wykonać TDD, oznacza to, że będę miał jeden gigantyczny zatwierdzenie (i przegląd kodu) dla tej funkcji, co jest złym pomysłem. Jak można tego uniknąć? Napisz część tej funkcji jako test i cały związany z nią kod, a następnie stopniowo dodawaj pozostałą część funkcji w kolejnych zatwierdzeniach?
Jordania
Naprawdę chciałbym zobaczyć prawdziwy przykład testów łączących się z implementacją.
PositiveGuy,
7

Moja interpretacja tego wystąpienia jest następująca:

  • komponenty testowe, a nie klasy.
  • testuj komponenty przez ich porty interfejsu.

Nie jest to określone w wykładzie, ale myślę, że założony kontekst porady jest podobny:

  • tworzysz system dla użytkowników, a nie, powiedzmy, bibliotekę narzędziową lub platformę.
  • Celem badania jest skutecznie dostarczać jak najwięcej w ramach budżetu konkurencyjnej.
  • komponenty są pisane w jednym, dojrzałym, prawdopodobnie statycznie typowanym języku, takim jak C # / Java.
  • komponent jest rzędu 10000-50000 linii; projekt Maven lub VS, wtyczka OSGI itp.
  • komponenty są pisane przez jednego programistę lub ściśle zintegrowany zespół.
  • kierujesz się terminologią i podejściem przypominającym architekturę sześciokątną
  • port komponentu to miejsce, w którym zostawiasz język lokalny, a jego system typów, przełączając się na http / SQL / XML / bytes / ...
  • Zawijanie każdego portu to interfejsy maszynowe, w sensie Java / C #, w których implementacje mogą być przełączane w celu przełączania technologii.

Zatem testowanie komponentu jest największym możliwym zakresem, w którym coś nadal można rozsądnie nazwać testowaniem jednostkowym. Różni się to raczej od tego, jak niektórzy ludzie, zwłaszcza akademicy, używają tego terminu. Nie przypomina to przykładów w typowym samouczku do testów jednostkowych. Jednak odpowiada to swojemu pochodzeniu podczas testowania sprzętu; płyty i moduły są testowane jednostkowo, a nie przewody i śruby. A przynajmniej nie budujesz fałszywego Boeinga do testowania śruby ...

Wyciągając z tego ekstrapolację i wtrącając własne myśli,

  • Każdy interfejs będzie wejściem, wyjściem lub współpracownikiem (jak baza danych).
  • Ci przetestować interfejsy wejściowe; wywołać metody, potwierdzić wartości zwracane.
  • Państwo szydzić interfejsów wyjściowych; sprawdź, czy oczekiwane metody są wywoływane dla danego przypadku testowego.
  • Państwo fałszywe współpracownicy; zapewnić proste, ale działające wdrożenie

Jeśli zrobisz to właściwie i czysto, ledwo potrzebujesz narzędzia kpiącego; zużywa się tylko kilka razy na system.

Baza danych jest zazwyczaj współpracownikiem, więc zostaje sfałszowana, a nie wyśmiewana. Wykonanie tego byłoby bolesne ręcznie; na szczęście takie rzeczy już istnieją .

Podstawowym wzorcem testowym jest wykonanie sekwencji operacji (np. Zapisanie i ponowne załadowanie dokumentu); potwierdź, że działa. Jest to to samo, co w przypadku każdego innego scenariusza testowego; żadna (działająca) zmiana implementacji prawdopodobnie nie spowoduje niepowodzenia takiego testu.

Wyjątkiem są zapisy bazy danych, które nigdy nie są odczytywane przez testowany system; np. dzienniki kontroli lub podobne. Są to dane wyjściowe i dlatego powinny być wyśmiewane. Wzorzec testowy wykonuje pewną sekwencję operacji; potwierdź, że interfejs kontroli został wywołany przy użyciu metod i argumentów określonych.

Zauważ, że nawet tutaj, pod warunkiem, że używasz bezpiecznego narzędzia do kpin typu mockito , zmiana nazwy metody interfejsu nie może spowodować niepowodzenia testu. Jeśli użyjesz IDE z załadowanymi testami, zostanie ono refaktoryzowane wraz ze zmianą nazwy metody. Jeśli nie, test nie zostanie skompilowany.

soru
źródło
Czy możesz opisać / podać konkretny przykład portu interfejsu?
PositiveGuy,
jaki jest przykład interfejsu wyjściowego. Czy możesz być konkretny w kodzie? To samo z interfejsem wejściowym.
PositiveGuy,
Interfejs (w sensie Java / C #) otacza port, którym może być wszystko, co mówi do świata zewnętrznego (d / b, gniazdo, http, ....). Interfejs wyjściowy to taki, który nie ma metod z wartościami zwracanymi ze świata zewnętrznego za pośrednictwem portu, tylko wyjątki lub równoważne.
soru
Interfejs wejściowy jest odwrotny, współpracownik jest zarówno wejściowy, jak i wyjściowy.
soru
1
Myślę, że mówisz o zupełnie innym podejściu do projektowania i zestawie terminologii niż ta opisana na filmie. Ale w 90% przypadków repozytorium (tj. Baza danych) jest współpracownikiem, a nie danymi wejściowymi lub wyjściowymi. A zatem interfejs do niego jest interfejsem współpracy.
soru
0

Moją sugestią jest zastosowanie podejścia testowego opartego na stanie:

DANE Mamy testową bazę danych DB w znanym stanie

KIEDY Usługa jest wywoływana z argumentami X

NASTĘPNIE Potwierdź, że baza danych zmieniła się ze stanu pierwotnego do stanu oczekiwanego, wywołując metody repozytorium tylko do odczytu i sprawdzając ich zwrócone wartości

W ten sposób nie polegasz na żadnym wewnętrznym algorytmie usługi i możesz dowolnie refaktoryzować jej implementację bez konieczności zmiany testów.

Jedynym sprzężeniem jest tutaj wywołanie metody usługi i wywołania repozytorium potrzebne do odczytania danych z bazy danych, co jest w porządku.

Elifarley
źródło