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_oncenie powiodło się, to pokazuje inny kluczowy aspektMockobiektów, rejestrują one wszystkie interakcje z nimi, a następnie można sprawdzić te interakcje.

na przykład możesz użyć call_countaby pobrać liczbę wywołań do MockI użyć call_argslub 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.

patchprzechwyci importinstrukcje 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 importujemyosI wywołujemygetcwdaby 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 patchzadział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.,

patchprzekaże argumenty słów kluczowych do klasyMock, tak aby Konfiguracja areturn_valuepo 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_valuew przykładzie po prostu użycieMockHelper.get_path.return_valuenie zadziała, ponieważ w kodzie wywołujemyget_pathna 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ą:

    Mockjest 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.objectpozwala 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:

    1. 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.mockaby utworzyćMock mimo że żaden import nie pasuje do identyfikatora.
    2. 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ć.,
    3. 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.dictnie 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 jakoStringIOobiekt:

    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:

    1. 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.,
    2. 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żywaFieldpoprzez zdefiniowanie jejcountryatrybut jako taki, którego typem jeststrIdefaultjako 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:

    1. najpierw łatki domyślne kraje w pricer być lista z pojedynczym wpisem GB ,
    2. to powinno sprawić, że CountryPricer.country atrybut domyślny dla tego wpisu, ponieważ definicja obejmuje default=COUNTRIES,
    3. 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:

    1. instancja country atrybut Field jest budowana przed uruchomieniem testu w czasie importu,
    2. Python odczytuje ciało klasy, przekazując iv id
      to jest zdefiniowane w tym momencie do Fieldinstancja

      ,

    3. 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.patchzaimportuje 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 zpytestmonkeypatch. 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