to po prostu fakt, ponieważ kod rośnie w końcu będziesz musiał zacząć dodawać mocki do swojego zestawu testów. To, co zaczęło się jako ładny mały projekt z dwiema klasami, teraz rozmawia z zewnętrznymi usługami i nie można go już wygodnie testować.
dlatego Python dostarczaunittest.mock
, potężną część standardowej biblioteki do stubowania zależności i wyśmiewania efektów ubocznych.
jednakunittest.mock
nie jest szczególnie intuicyjny.,
wiele razy zastanawiałam się, dlaczego mój przepis nie działa w konkretnym przypadku, więc zebrałam ten cheatsheet, aby pomóc sobie i innym szybko zacząć działać.
przykłady kodu można znaleźć w repozytorium Github artykułu. Będę używał Pythona 3.6, jeśli używasz 3.2 lub poniżej musisz użyćmock
Pakiet PyPI.,
przykłady są pisane za pomocąunittest.TestCase
klas dla uproszczenia w wykonywaniu ich bez zależności, ale można je zapisać jako funkcje za pomocąpytest
prawie bezpośrednio,unittest.mock
będzie działać dobrze. Jeśli jesteś użytkownikiem pytest
zachęcam jednak do zajrzenia do doskonałej biblioteki pytest-mock
.
punktem centralnym modułuunittest.mock
jest oczywiście KlasaMock
., Główną cechą obiektu Mock
jest to, że zwróci on inną instancję Mock
, gdy:
dostęp do jednego z jego atrybutów
wywołanie samego obiektu
jest to domyślne zachowanie, ale może być nadpisane na różne sposoby. Na przykład możesz przypisać wartość do atrybutu w Mock
przez:
przypisz ją bezpośrednio, tak jak w przypadku każdego obiektu Pythona.,
użyj metodyconfigure_mock
na instancji.
lub przekazać argumenty słów kluczowych do klasyMock
podczas tworzenia.
aby nadpisać wywołania makiety, musisz skonfigurować jego właściwość return_value
, dostępną również jako argument słowa kluczowego w Mock
inicjalizator., Mock
zawsze zwróci tę samą wartość we wszystkich wywołaniach, można ją również skonfigurować za pomocą atrybutu side_effect
:
jeśli chcesz zwrócić różne wartości przy każdym wywołaniu, możesz przypisać iterable do side_effect
.
Jeśli chcesz wywołać wyjątek podczas wywoływania Mocka, możesz po prostu przypisać obiekt wyjątku do side_effect
.,
za pomocą tych wszystkich narzędzi możemy teraz tworzyć stuby dla praktycznie każdego obiektu Pythona, który będzie działał świetnie dla wejść do naszego systemu. Ale co z wyjściami?
Jeśli robisz program CLI, aby pobrać cały Internet, prawdopodobnie nie chcesz pobierać całego Internetu na każdym teście. Zamiast tego wystarczy stwierdzić, że requests.download_internet
(nie jest to prawdziwa metoda) została odpowiednio wywołana. Mock
daje przydatne metody, aby to zrobić.,
Uwaga w naszym przykładzieassert_called_once
nie powiodło się, to pokazuje inny kluczowy aspektMock
obiektów, rejestrują one wszystkie interakcje z nimi, a następnie można sprawdzić te interakcje.
na przykład możesz użyć call_count
aby pobrać liczbę wywołań do Mock
I użyć call_args
lub call_args_list
aby sprawdzić argumenty odpowiednio do ostatnich lub wszystkich wywołań.,
Jeśli jest to niewygodne w dowolnym momencie możesz użyć metodyreset_mock
do wyczyszczenia nagranych interakcji, zauważ, że konfiguracja nie zostanie zresetowana, tylko interakcje.
wreszcie przedstawięMagicMock
, podklasęMock
, która implementuje domyślne metody magic lub Dunder. To sprawia, że MagicMock
jest idealna do naśladowania zachowania klas, dlatego jest to klasa domyślna podczas patchowania.,
jesteśmy już gotowi do rozpoczęcia testów i izolacji urządzenia. Oto kilka przepisów, o których należy pamiętać:
łatanie importu
głównym sposobem użycia unittest.mock
jest łatanie importu w testowanym module przy użyciu funkcji patch
.
patch
przechwyci import
instrukcje zidentyfikowane za pomocą ciągu znaków (więcej o tym później) i zwróci Mock
instancję można wstępnie skonfigurować za pomocą technik omówionych powyżej.,
wyobraź sobie, że chcemy przetestować tę bardzo prostą funkcję:
Uwaga importujemyos
I wywołujemygetcwd
aby pobrać bieżący katalog roboczy. Nie chcemy jednak wywoływać tego w naszych testach, ponieważ nie jest to ważne dla naszego kodu, a wartość zwracana może się różnić w zależności od środowiska, na którym działa.
jak wspomniano powyżej musimy dostarczyćpatch
z łańcuchem reprezentującym nasz konkretny import., Nie chcemy dostarczać po prostu os.getcwd
, ponieważ byłoby to łatką dla wszystkich modułów , zamiast tego chcemy dostarczyć moduł pod testowym import
z os
, tzn. work.os
. Po zaimportowaniu modułu patch
zadziała magicznie i zwróci Mock
.,
alternatywnie możemy użyć wersji dekoratora patch
, zauważ , że tym razem test ma dodatkowy parametr: mocked_os
do którego wstrzykuje się do testu Mock
.,
patch
przekaże argumenty słów kluczowych do klasyMock
, tak aby Konfiguracja areturn_value
po prostu dodajemy go jako jeden:
szydzenie klas
jest to dość powszechne do łatania klas całkowicie lub częściowo., Helper
z "db"
Worker
zwraca oczekiwaną ścieżkę dostarczoną przez Helper
w celu przetestowania Worker
w pełnej izolacji musimy łatać całość Helper
class:
zwróć uwagę na podwójnyreturn_value
w przykładzie po prostu użycieMockHelper.get_path.return_value
nie zadziała, ponieważ w kodzie wywołujemyget_path
na instancji, a nie na samej klasie.,
składnia łańcucha jest nieco myląca, ale pamiętaj, żeMagicMock
zwraca innyMagicMock
podczas wywołań__init__
. Tutaj konfigurujemy wszelkie fałszywe wystąpieniaHelper
utworzone przezMockHelper
, aby zwracały to, czego oczekujemy przy wywołaniachget_path
, co jest jedyną metodą, na której nam zależy w naszym teście.,
Class speccing
konsekwencją elastycznościMock
jest to, że po wyśmianiu klasy Python nie podniesieAttributeError
, ponieważ po prostu zwróci nowe instancjeMagicMock
dla zasadniczo wszystkiego. Zwykle jest to dobra rzecz, ale może prowadzić do mylących zachowań i potencjalnie błędów. Na przykład pisząc poniższy test,
po cichu przejdzie bez ostrzeżenia, całkowicie brakuje literówki w assrt_called_once
.,
dodatkowo, gdybyśmy zmienili nazwę Helper.get_path
na Helper.get_folder
, ale zapomnieliśmy zaktualizować wywołanie w Worker
nasze testy nadal przebiegną:
Mock
jest wyposażony w narzędzie, aby zapobiec tym błędom, speccing.
mówiąc prościej, wstępnie skonfiguruje mocks, aby odpowiadał tylko na metody, które faktycznie istnieją w klasie spec., Istnieje kilka sposobów definiowania specyfikacji, ale najłatwiej jest po prostu przekazać autospec=True
do wywołaniapatch
, które skonfiguruje Mock
, aby zachowywać się jak obiekt, który jest wyśmiewany, zgłaszając wyjątki dla brakujących atrybutów i nieprawidłowych podpisów zgodnie z wymaganiami., Na przykład:
częściowe wyśmiewanie klas
Jeśli jesteś mniej skłonny do testowania w pełnej izolacji, możesz również częściowo poprawić klasę używając patch.object
:
tutajpatch.object
pozwala nam skonfigurować wyśmiewaną wersję get_path
tylko, pozostawiając resztę zachowania nietkniętą., Oczywiście oznacza to, że test nie jest już tym, co uważałbyś za test jednostkowy, ale możesz być z tym ok.
wyśmiewanie wbudowanych funkcji i zmiennych środowiskowych
w poprzednich przykładach zaniedbaliśmy przetestować jeden szczególny aspekt naszej prostej klasy,print
wywołanie. Jeśli w kontekście Twojej aplikacji jest to ważne, jak na przykład polecenie CLI, musimy dokonać twierdzeń przeciwko temu wywołaniu.,
print
jest oczywiście wbudowaną funkcją w Pythonie, co oznacza, że nie musimy jej importować do naszego modułu, co jest sprzeczne z tym, co omówiliśmy powyżej na temat łatania przy imporcie., Wyobraź sobie, że mieliśmy nieco bardziej skomplikowaną wersję naszej funkcji:
możemy napisać test w następujący sposób:
zwróć uwagę na kilka rzeczy:
możemy bez problemu wyśmiewać print
I twierdzić, że doszło do wywołania, postępując zgodnie z regułą „patch on import”. Była to jednak zmiana wprowadzona w 3.,5, wcześniej trzeba było dodać create=True
dopatch
wywołanie sygnałuunittest.mock
aby utworzyćMock
mimo że żaden import nie pasuje do identyfikatora.
używamy patch.dict
, aby wstrzyknąć tymczasową zmienną środowiskową wos.environ
, jest to możliwe do rozszerzenia na każdy inny słownik, który chcemy naśladować.,
zagnieżdżamy kilka wywołań menedżera kontekstu patch
, ale używamy tylko as
w pierwszym z nich, ponieważ jest to ten, którego musimy użyć do wywołania assert_called_once_with
.,
Jeśli nie lubisz zagnieżdżać menedżerów kontekstu, możesz również napisać patch
wywołania w postaci dekoratora:
należy jednak zauważyć, że kolejność argumentów do testu odpowiada kolejności układania dekoratorów, a także, żepatch.dict
nie wprowadza argumentu.,ten bardzo często spotykany scenariusz:
możesz oczywiście dodać rzeczywisty plik konfiguracji, ale w rzeczywistych przypadkach może to nie być opcja, zamiast tego możemy wyśmiewać wyjście menedżera kontekstu jakoStringIO
obiekt:
nie ma tu nic specjalnego, oprócz magicznej metody__enter__
podstawowa mechanika menedżerów kontekstowych i sprytne wykorzystanie naszego zaufanego MagicMock
.,
wyśmiewanie atrybutów klasy
istnieje wiele sposobów, aby to osiągnąć, ale niektóre są bardziej niezawodne niż inne. Załóżmy, że napisałeś następujący kod:
możesz przetestować kod bez żadnych moków na dwa sposoby:
Jeśli kod testowany uzyskuje dostęp do atrybutu przez self.ATTRIBUTE
, co ma miejsce w tym przykładzie, można po prostu ustawić atrybut bezpośrednio w instancji. Jest to dość bezpieczne, ponieważ zmiana ogranicza się do tego pojedynczego wystąpienia.,
Alternatywnie można również ustawić atrybut w zaimportowanej klasie w teście przed utworzeniem instancji. To jednak zmienia atrybut klasy zaimportowany w teście, co może mieć wpływ na następujące testy, więc musisz pamiętać, aby go zresetować.
główną wadą tego podejścia nie-Mock jest to, że jeśli w dowolnym momencie zmienisz nazwę atrybutu, twoje testy się nie powiedzą, a błąd nie wskaże bezpośrednio tej niedopasowania nazewnictwa.,
aby rozwiązać ten problem, możemy użyć patch.object
na zaimportowanej klasie, która będzie narzekać, jeśli klasa nie ma określonego atrybutu.
oto kilka testów wykorzystujących każdą technikę:
wyśmiewanie helperów klas
poniższy przykład jest źródłem wielu problemów dotyczących monkeypatchingu za pomocą Mocka. Zwykle pojawia się na bardziej dojrzałych bazach kodowych, które zaczynają korzystać z frameworków i pomocników w definicji klasy., Na przykład, wyobraź sobie ten hipotetyczny Field
class helper:
jego celem jest zawinięcie i wzmocnienie atrybutu w inną klasę, dość wzór, który zwykle można zobaczyć w ORMs lub formie biblioteki. Nie przejmuj się zbytnio szczegółami, po prostu zauważ, że istniejetype
Idefault
przekazana wartość.,
teraz weź tę inną próbkę kodu produkcji:
Ta klasa używaField
poprzez zdefiniowanie jejcountry
atrybut jako taki, którego typem jeststr
Idefault
jako pierwszy element stałejCOUNTRIES
. Testowana logika polega na tym, że w zależności od kraju stosuje się zniżkę.,
dla których możemy napisać następujący test:
ale to nie przejdzie.,
przejdźmy przez test:
najpierw łatki domyślne kraje w pricer
być lista z pojedynczym wpisem GB
,
to powinno sprawić, że CountryPricer.country
atrybut domyślny dla tego wpisu, ponieważ definicja obejmuje default=COUNTRIES
,
następnie instanciates CountryPricer
i prosi o obniżoną cenę dla GB
!
więc o co chodzi?,
główną przyczyną tego jest zachowanie Pythona podczas importu, najlepiej opisane w znakomitym Pythonie Luciano Ramalho w rozdziale 21:
dla klas historia jest inna: w czasie importu interpreter wykonuje ciało każdej klasy, nawet ciało klas zagnieżdżonych w innych klasach. Wykonanie ciała klasy oznacza, że atrybuty i metody klasy są definiowane, a następnie budowany jest sam obiekt klasy.,
zastosowanie tego do naszego przykładu:
instancja country
atrybut Field
jest budowana przed uruchomieniem testu w czasie importu,
Python odczytuje ciało klasy, przekazując iv id
to jest zdefiniowane w tym momencie do Field
instancja
,
nasz kod testowy jest uruchomiony, ale jest już za późno na łatanieCOUNTRIES
i uzyskanie właściwego twierdzenia.,
Z powyższego opisu możesz spróbować opóźnić import modułu do wewnątrz testów, coś w stylu:
to jednak nadal nie przejdzie jakomock.patch
zaimportuje moduł, a następnie monkeypatch, co spowoduje taką samą sytuację jak wcześniej.,
aby obejść ten problem, musimy przyjąć stan CountryPricer
klasy w czasie testu i poprawki default
na już zainicjowanej instancji Field
:
To rozwiązanie nie jest idealne, ponieważ wymaga znajomości wewnętrznych funkcjiField
, których możesz nie mieć lub chcesz używać, jeśli używasz zewnętrznej biblioteki, ale działa na tym co prawda uproszczonym przypadku.,
Ten problem z czasem importu jest jednym z głównych problemów, które napotkałem podczas korzystania zunittest.mock
. Należy pamiętać przy jego użyciu, że w czasie importu jest wykonywany kod na najwyższym poziomie modułów, w tym organów klasowych.
Jeśli logika, którą testujesz, zależy od którejkolwiek z tych Logik, może być konieczne przemyślenie, w jaki sposób używaszpatch
.,
Ten wstęp i cheatsheet powinny Cię wyśmiać unittest.mock
, ale zachęcam do dokładnego zapoznania się z dokumentacją, jest wiele ciekawszych trików do nauczenia się.
warto wspomnieć, że istnieją alternatywy dlaunittest.mock
, w szczególności biblioteki Alexa Gaynorapretend
w połączeniu zpytest
monkeypatch
. Jak podkreśla Alex, korzystając z tych dwóch bibliotek, możesz przyjąć inne podejście, aby wyśmiewanie było bardziej rygorystyczne, a jednocześnie bardziej przewidywalne., Zdecydowanie podejście warte zbadania, ale poza zakresem tego artykułu.
unittest.mock
jest obecnie standardem szydzenia w Pythonie i znajdziesz go w praktycznie każdej bazie kodowej. Solidne zrozumienie tego pomoże Ci szybciej pisać lepsze testy.
Articles
Zobacz wpisy