Kiedy mam kpić?

137

Mam podstawową wiedzę mock i fałszywych obiektów, ale nie jestem pewien, mam przeczucie kiedy / gdzie używać szyderczy - zwłaszcza, że to stosuje się do tego scenariusza tutaj .

Esteban Araya
źródło
Zalecam mockowanie tylko zależności poza procesem i tylko tych z nich, z którymi można obserwować interakcje zewnętrznie (serwer SMTP, magistrala wiadomości itp.). Nie kpij z bazy danych, to szczegół implementacji. Więcej na ten temat tutaj: enterprisecraftsmanship.com/posts/when-to-mock
Vladimir

Odpowiedzi:

121

Test jednostkowy powinien testować pojedynczą ścieżkę kodową za pomocą jednej metody. Kiedy wykonanie metody przechodzi poza tę metodę, do innego obiektu i z powrotem, istnieje zależność.

Testując tę ​​ścieżkę kodu z rzeczywistą zależnością, nie wykonujesz testów jednostkowych; jesteście testami integracyjnymi. Chociaż jest to dobre i konieczne, nie jest to testowanie jednostkowe.

Jeśli Twoja zależność jest błędna, może to wpłynąć na wynik testu w taki sposób, że zwróci fałszywie dodatni wynik. Na przykład możesz przekazać zależności nieoczekiwaną wartość null, a zależność może nie zostać wyrzucona na wartość null, jak zostało udokumentowane. Twój test nie napotyka wyjątku zerowego argumentu, tak jak powinien, i test kończy się pomyślnie.

Ponadto może być trudne, jeśli nie niemożliwe, niezawodne spowodowanie, aby obiekt zależny zwrócił dokładnie to, co chcesz podczas testu. Obejmuje to również rzucanie oczekiwanych wyjątków w testach.

Mock zastępuje tę zależność. Ustawiasz oczekiwania dotyczące wywołań obiektu zależnego, ustawiasz dokładne wartości zwracane, które powinny Ci dać, aby wykonać żądany test i / lub jakie wyjątki zgłosić, aby móc przetestować kod obsługi wyjątków. W ten sposób można łatwo przetestować dane urządzenie.

TL; DR: Mock każdą zależność, której dotyka twój test jednostkowy.

Drew Stephens
źródło
164
Ta odpowiedź jest zbyt radykalna. Testy jednostkowe mogą i powinny wykonywać więcej niż jedną metodę, o ile wszystkie należą do tej samej spójnej jednostki. W przeciwnym razie wymagałoby to zbyt wielu kpin / udawania, co prowadziłoby do skomplikowanych i delikatnych testów. Tylko te zależności, które tak naprawdę nie należą do testowanej jednostki, powinny zostać zastąpione przez mockowanie.
Rogério
10
Ta odpowiedź jest również zbyt optymistyczna. Byłoby lepiej, gdyby zawierał mankamenty @Jana dotyczące pozorowanych obiektów.
Jeff Axelrod
1
Czy nie jest to raczej argument za wstrzykiwaniem zależności do testów, a nie konkretnie makietami? W swojej odpowiedzi można by właściwie zastąpić „mock” przez „stub”. Zgadzam się, że powinieneś albo kpić, albo stłumić istotne zależności. Widziałem wiele makietowego kodu, który w zasadzie kończy się reimplementacją części mockowanych obiektów; kpiny z pewnością nie są srebrną kulą.
Draemon,
2
Mock każdą zależność, której dotyka test jednostkowy. To wszystko wyjaśnia.
Teoman shipahi
2
TL; DR: Mock każdą zależność, której dotyka twój test jednostkowy. - to naprawdę nie jest świetne podejście, mówi samo mockito - nie kpij sobie ze wszystkiego. (downvoted)
p_champ,
167

Obiekty pozorowane są przydatne, gdy chcesz przetestować interakcje między testowaną klasą a określonym interfejsem.

Na przykład chcemy przetestować sendInvitations(MailServer mailServer)wywołania tej metody MailServer.createMessage()dokładnie raz, a także wywołania MailServer.sendMessage(m)dokładnie raz, a żadne inne metody nie są wywoływane w MailServerinterfejsie. Wtedy możemy użyć pozorowanych obiektów.

Mockując obiekty, zamiast zdawać rzeczywisty MailServerImpllub test TestMailServer, możemy przekazać symulowaną implementację MailServerinterfejsu. Zanim przejdziemy do makiety MailServer, „trenujemy” ją, aby wiedziała, jakiej metody się spodziewać i jakie wartości zwracane. Na koniec obiekt pozorowany stwierdza, że ​​wszystkie oczekiwane metody zostały wywołane zgodnie z oczekiwaniami.

W teorii brzmi to dobrze, ale są też pewne wady.

Pozorowane niedociągnięcia

Jeśli masz na miejscu makietę frameworka, kusi Cię używanie obiektu mock za każdym razem , gdy musisz przekazać interfejs do testowanej klasy. W ten sposób kończysz testowanie interakcji, nawet jeśli nie jest to konieczne . Niestety, niechciane (przypadkowe) testowanie interakcji jest złe, ponieważ wtedy testujesz, że dane wymaganie jest zaimplementowane w określony sposób, zamiast tego, że implementacja dała wymagany wynik.

Oto przykład w pseudokodzie. Załóżmy, że stworzyliśmy MySorterklasę i chcemy ją przetestować:

// the correct way of testing
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert testList equals [1, 2, 3, 7, 8]
}


// incorrect, testing implementation
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert that compare(1, 2) was called once 
    assert that compare(1, 3) was not called 
    assert that compare(2, 3) was called once 
    ....
}

(W tym przykładzie zakładamy, że nie chcemy testować określonego algorytmu sortowania, takiego jak sortowanie szybkie; w takim przypadku ten drugi test byłby faktycznie prawidłowy).

W tak skrajnym przykładzie jest oczywiste, dlaczego ten drugi przykład jest błędny. Kiedy zmieniamy implementację MySorter, pierwszy test wykonuje świetną robotę, upewniając się, że nadal poprawnie sortujemy, co jest celem testów - pozwalają nam one bezpiecznie zmieniać kod. Z drugiej strony ten ostatni test zawsze się psuje i jest aktywnie szkodliwy; utrudnia refaktoryzację.

Mocks as stubs

Mock frameworki często pozwalają również na mniej rygorystyczne użycie, gdzie nie musimy dokładnie określać, ile razy metody powinny być wywoływane i jakich parametrów się oczekuje; pozwalają na tworzenie pozorowanych obiektów, które są używane jako kody pośredniczące .

Załóżmy, że mamy metodę sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer), którą chcemy przetestować. PdfFormatterObiekt może być wykorzystywane do tworzenia zaproszenie. Oto test:

testInvitations() {
   // train as stub
   pdfFormatter = create mock of PdfFormatter
   let pdfFormatter.getCanvasWidth() returns 100
   let pdfFormatter.getCanvasHeight() returns 300
   let pdfFormatter.addText(x, y, text) returns true 
   let pdfFormatter.drawLine(line) does nothing

   // train as mock
   mailServer = create mock of MailServer
   expect mailServer.sendMail() called exactly once

   // do the test
   sendInvitations(pdfFormatter, mailServer)

   assert that all pdfFormatter expectations are met
   assert that all mailServer expectations are met
}

W tym przykładzie tak naprawdę nie obchodzi nas PdfFormatterobiekt, więc po prostu trenujemy go, aby po cichu akceptował każde wywołanie i zwracał pewne rozsądne wartości zwracane w puszkach dla wszystkich metod, które sendInvitation()akurat wywołują w tym momencie. Jak wymyśliliśmy dokładnie tę listę metod treningu? Po prostu uruchomiliśmy test i dodawaliśmy metody aż do pomyślnego zakończenia testu. Zauważ, że wytrenowaliśmy kod pośredniczący, aby odpowiadał na metodę bez pojęcia, dlaczego musi ją wywoływać, po prostu dodaliśmy wszystko, na co test narzekał. Cieszymy się, test przeszedł pomyślnie.

Ale co dzieje się później, gdy zmienimy sendInvitations()lub inną klasę, która sendInvitations()używa, do tworzenia bardziej fantazyjnych plików PDF? Nasz test nagle kończy się niepowodzeniem, ponieważ teraz PdfFormatterwywoływanych jest więcej metod i nie wytrenowaliśmy naszego kodu, aby ich oczekiwać. Zwykle nie jest to tylko jeden test, który kończy się niepowodzeniem w takich sytuacjach, ale każdy test, który używa, bezpośrednio lub pośrednio, sendInvitations()metody. Musimy naprawić wszystkie te testy, dodając więcej szkoleń. Zauważ również, że nie możemy usunąć metod, które nie są już potrzebne, ponieważ nie wiemy, które z nich nie są potrzebne. Znowu utrudnia to refaktoryzację.

Również czytelność testu bardzo ucierpiała, jest tam dużo kodu, którego nie napisaliśmy, ponieważ chcieliśmy, ale ponieważ musieliśmy; to nie my chcemy tam tego kodu. Testy wykorzystujące pozorowane obiekty wyglądają na bardzo złożone i często są trudne do odczytania. Testy powinny pomóc czytelnikowi zrozumieć, w jaki sposób należy korzystać z klasy będącej przedmiotem testu, dlatego powinny być proste i zrozumiałe. Jeśli nie są czytelne, nikt nie będzie ich konserwował; w rzeczywistości łatwiej je usunąć niż je utrzymywać.

Jak to naprawić? Z łatwością:

  • Jeśli to możliwe, staraj się używać prawdziwych klas zamiast kpiny. Użyj prawdziwego PdfFormatterImpl. Jeśli nie jest to możliwe, zmień prawdziwe klasy, aby było to możliwe. Brak możliwości użycia klasy w testach zwykle wskazuje na pewne problemy z klasą. Naprawianie problemów to sytuacja, w której wszyscy wygrywają - naprawiłeś klasę i masz prostszy test. Z drugiej strony, nie naprawianie go i używanie makiet jest sytuacją bez wyjścia - nie naprawiłeś prawdziwej klasy i masz bardziej złożone, mniej czytelne testy, które utrudniają dalsze refaktoryzacje.
  • Spróbuj utworzyć prostą implementację testową interfejsu, zamiast kpić z niego w każdym teście, i używaj tej klasy testowej we wszystkich testach. Stwórz TestPdfFormatter, który nic nie robi. W ten sposób możesz zmienić to raz dla wszystkich testów, a twoje testy nie są zaśmiecone długimi konfiguracjami, w których trenujesz swoje kody.

Podsumowując, pozorowane obiekty mają swoje zastosowanie, ale jeśli nie są używane ostrożnie, często zachęcają do złych praktyk, testowania szczegółów implementacji, utrudniają refaktoryzację i tworzą trudne do odczytania i trudne do utrzymania testy .

Aby uzyskać więcej informacji na temat niedociągnięć makiet, zobacz także Mock Objects: Shortcomings and Use Cases .

Jan Soltis
źródło
1
Dobrze przemyślana odpowiedź i w większości się zgadzam. Powiedziałbym, że ponieważ testy jednostkowe są testami białoskrzynkowymi, konieczność zmiany testów po zmianie implementacji w celu wysyłania bardziej wyszukanych plików PDF może nie być nieuzasadnionym obciążeniem. Czasami makiety mogą być przydatnym sposobem na szybkie wdrożenie odcinków zamiast posiadania dużej ilości kotłów. W praktyce wydaje się, że ich użycie nie jest jednak powtórzone do tych prostych przypadków.
Draemon,
1
Czyż nie chodzi o to, że próba polega na tym, że twoje testy są spójne, że nie musisz się martwić o mockowanie obiektów, których implementacje nieustannie się zmieniają, być może przez innych programistów za każdym razem, gdy uruchamiasz test i uzyskujesz spójne wyniki testów?
PositiveGuy
1
Bardzo dobre i istotne punkty (szczególnie dotyczące testów kruchości). Kiedy byłem młodszy,
często używałem mocków
6
„Brak możliwości użycia klasy w testach zwykle wskazuje na pewne problemy z klasą”. Jeśli klasa jest usługą (np dostęp do bazy danych lub pełnomocnika do serwisu WWW), należy uznać, jako zewnętrznego dependancy i szydzili / zgaszone
Michael Freidgeim
1
Ale co dzieje się później, gdy zmienimy sendInvitations ()? Jeśli testowany kod zostanie zmodyfikowany, nie gwarantuje to już wcześniejszej umowy, dlatego musi zawieść. I zwykle nie jest to tylko jeden test, który kończy się niepowodzeniem w takich sytuacjach . W takim przypadku kod nie jest zaimplementowany w sposób czysty. Weryfikacja wywołań metod zależności powinna być testowana tylko raz (w odpowiednim teście jednostkowym). Wszystkie inne klasy będą używać tylko instancji pozorowanej. Nie widzę więc żadnych korzyści mieszania integracji z testami jednostkowymi.
Christopher Will
55

Praktyczna zasada:

Jeśli testowana funkcja wymaga skomplikowanego obiektu jako parametru i trudno byłoby po prostu utworzyć instancję tego obiektu (jeśli, na przykład, próbuje nawiązać połączenie TCP), użyj makiety.

Orion Edwards
źródło
4

Powinieneś mockować obiekt, jeśli masz zależność w jednostce kodu, którą próbujesz przetestować, która musi być „właśnie taka”.

Na przykład, gdy próbujesz przetestować jakąś logikę w swojej jednostce kodu, ale musisz uzyskać coś z innego obiektu, a to, co zostanie zwrócone z tej zależności, może wpłynąć na to, co próbujesz przetestować - mock ten obiekt.

Świetny podcast na ten temat można znaleźć tutaj

Toran Billups
źródło
Łącze prowadzi teraz do bieżącego odcinka, a nie do zamierzonego odcinka. Czy zamierzony podcast to hanselminutes.com/32/mock-objects ?
C Perkins