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 .
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 .
Odpowiedzi:
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.
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 MailServer
interfejsie. Wtedy możemy użyć pozorowanych obiektów.
Mockując obiekty, zamiast zdawać rzeczywisty MailServerImpl
lub test TestMailServer
, możemy przekazać symulowaną implementację MailServer
interfejsu. 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.
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 MySorter
klasę 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ę.
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ć. PdfFormatter
Obiekt 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 PdfFormatter
obiekt, 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 PdfFormatter
wywoł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ą:
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.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 .
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.
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