Wyśmiewanie w Pythonie: unittest.mock, patch i błędy, które sprawiają, że testy kłamią

  1. Strona główna
  2. Blog
  3. Automatyzacja testów

Automatyzacja testów

Wyśmiewanie w Pythonie: unittest.mock, patch i błędy, które sprawiają, że testy kłamią

Makiety pozwalają testować kod bez prawdziwej sieci, bazy danych i zegara. Używane nieostrożnie, przepuszczają również zepsuty kod. Oto jak dobrze używać unittest.mock przy każdym przykładowym uruchomieniu.

8 min czytania
Wyśmiewanie w Pythonie: unittest.mock, patch i błędy, które sprawiają, że testy kłamią

Najważniejsze

  • mock zastępuje prawdziwy obiekt, taki jak klient API, więc testy przebiegają szybko i w trybie offline.
  • Użyj return_value dla ustalonej odpowiedzi i side_effect dla błędów lub sekwencji.
  • Popraw nazwę tam, gdzie jest używana, a nie tam, gdzie jest zdefiniowana. Jest to najczęstszy błąd.
  • Użyj spec lub autospec, aby literówki i błędne argumenty zawiodły, zamiast przechodzić po cichu.

Twoja funkcja wywołuje interfejs API pogody. Twój test nie powinien zależeć od Internetu, klucza API ani jutrzejszej pogody. Wbudowana funkcja Pythona unittest.mock pozwala zastąpić to wywołanie funkcją zastępczą, która zwraca dokładnie to, co wybierzesz.

Napisaliśmy mały weather moduł i dziewięć testów, wszystkie uruchamiane za pomocą testu jednostkowego Pythona 3.13. Wszystkie dziewięć przeszło pomyślnie i poniższe wydruki pochodzą z tego przebiegu.

Dlaczego kpić?

  • Prędkość: brak połączeń sieciowych i konfiguracji bazy danych.
  • Niezawodność: testy nie kończą się niepowodzeniem z powodu awarii usługi innej firmy.
  • Kontrola: możesz na żądanie symulować przekroczenia limitu czasu, błędy i dziwne dane.
  • Izolacja: testujesz swoją logikę, a nie czyjś serwer.

Makiety należą głównie do testów jednostkowych. Testy integracyjne powinny nadal rozmawiać z rzeczywistymi lub realistycznymi zależnościami.

Próbne podstawy

from unittest.mock import Mock

api = Mock()
api.get_price.return_value = 499

print(api.get_price("pen"))                # 499
api.get_price.assert_called_once_with("pen")
print(api.get_price.call_args_list)        # [call('pen')]
print(api.anything_at_all())               # <Mock name='mock.anything_at_all()' ...>

A Mock rejestruje każde połączenie, dzięki czemu możesz sprawdzić, w jaki sposób zostało ono wykorzystane. Zwróć jednak uwagę na ostatnią linijkę. Zwykły Mock wymyśla dowolny atrybut, o który poprosisz. Ta elastyczność jest wygodna, ale także niebezpieczna, jak się przekonamy.

MagicMock to ten sam pomysł, ale obsługuje także metody „magiczne”, takie jak __len__ i __iter__. patch() domyślnie używa MagicMock.

side_effect: błędy i sekwencje

side_effect kontroluje, co dzieje się podczas każdego połączenia. Ustaw go na wyjątek, aby zasymulować awarię:

api.fetch.side_effect = ConnectionError("down")
api.fetch()   # raises ConnectionError

Ustaw go na listę, aby zwracać różne wyniki przy kolejnych połączeniach. Jest to idealne rozwiązanie do testowania logiki ponawiania prób:

def fetch_with_retry(api, attempts=3):
    for _ in range(attempts):
        try:
            return api.fetch()
        except ConnectionError:
            continue
    raise ConnectionError("all attempts failed")

api = Mock()
api.fetch.side_effect = [ConnectionError("busy"), {"ok": True}]
print(fetch_with_retry(api))    # {'ok': True}
print(api.fetch.call_count)     # 2

Pierwsze wywołanie nie powiodło się, drugie się powiodło, a test wykazał, że funkcja została ponowiona dokładnie raz. Symulacja tego w porównaniu z rzeczywistą usługą byłaby prawie niemożliwa.

patch(): tymczasowo zastępuje coś

Często zależność nie jest przekazywana. Jest importowana do wnętrza modułu. patch() zamienia go na czas trwania testu, a następnie przywraca:

# weather.py
from http_client import get as requests_get

def report(city):
    data = requests_get(f"https://api.example.com/weather?city={city}")
    return f"{city}: {data['temp_c']}°C"
from unittest.mock import patch

with patch("weather.requests_get") as fake_get:
    fake_get.return_value = {"temp_c": 31}
    print(weather.report("Pune"))   # Pune: 31°C
    fake_get.assert_called_once_with(
        "https://api.example.com/weather?city=Pune")

Możesz użyć patch jako menedżera kontekstu w ten sposób lub jako dekorator, który przekazuje próbkę jako argument.

Łatka tam, gdzie jest używana, a nie tam, gdzie jest zdefiniowana

To błąd, który prawie każdy popełnia raz. Prawdziwa funkcja get znajduje się w http_client, więc naturalnym wydaje się jej załatanie tam:

with patch("http_client.get") as fake_get:
    fake_get.return_value = {"temp_c": 99}
    weather.report("Pune")

# ConnectionError: no network in tests: https://api.example.com/weather?city=Pune

Prawdziwa funkcja została uruchomiona. Kiedy weather.py zrobił from http_client import get as requests_get, stworzył własną nazwę wskazującą na pierwotną funkcję. Patchowanie http_client.get zmieniło inną nazwę, więc weather nigdy nie widział tej kpiny. Nasze http_client.get podbić celowo, więc błąd był głośny. W prawdziwym projekcie może po cichu wywołać aktywne API.

Reguła: popraw nazwę testowanego modułu, zapisaną jako "module_under_test.name". Jeśli moduł wykonał import http_client i wywołał http_client.get(), to poprawka "http_client.get" jest prawidłowa, ponieważ wyszukiwanie odbywa się w czasie wywołania.

spec i autospec: spraw, aby drwiny były rygorystyczne

Zwykły Mock akceptuje wszystko, łącznie z literówkami. W przypadku specyfikacji dozwolone są tylko atrybuty, które posiada prawdziwa klasa:

api = Mock(spec=weather.WeatherClient)
api.get_temprature("Pune")
# AttributeError: Mock object has no attribute 'get_temprature'

loose = Mock()
loose.get_temprature("Pune")    # returns a Mock, no error at all

Luźna próbna wersja z radością zaakceptowała metodę z błędną pisownią, więc test z jej użyciem mógł zakończyć się pomyślnie, nawet jeśli prawdziwy kod uległby awarii. autospec idzie dalej i sprawdza sygnatury metod:

with patch("weather.WeatherClient", autospec=True) as Fake:
    client = Fake.return_value
    client.get_temperature("Pune", "extra", "args")
# TypeError: too many positional arguments

Preferuj autospec=True lub create_autospec(), gdy kpisz z własnych klas. Dzięki temu kpiny są uczciwe, gdy zmienia się prawdziwy kod.

Kpiące daty i godziny

Kod zależny od dzisiejszej daty jest trudny do przetestowania. Owiń zegar małą funkcją i załataj:

def today():
    return datetime.date.today()

def is_weekend():
    return today().weekday() >= 5

@patch("weather.today")
def test_date(self, fake_today):
    fake_today.return_value = datetime.date(2026, 11, 7)
    print(weather.is_weekend())   # True (a Saturday)

Nie można łatwo załatać bezpośrednio datetime.date.today, ponieważ typy wbudowane nie pozwalają na ustawienie ich atrybutów. Biblioteki takie jak freezegun i time-machine radzą sobie z tym pełniej.

Twierdzenia na kpinach

Metoda Kontrole
assert_called() Zadzwoniłem przynajmniej raz
assert_called_once() Zadzwoniłem dokładnie raz
assert_called_with(...) W ostatnim wywołaniu użyto tych argumentów
assert_called_once_with(...) Dokładnie jedno wywołanie z tymi argumentami
assert_any_call(...) Każde wywołanie korzystało z tych argumentów
assert_not_called() Nigdy nie dzwoniłem

Gdy asercja się nie powiedzie, komunikat jest jasny. Wywołaliśmy assert_called_once() metodę, która nigdy nie działała:

AssertionError: Expected 'save' to have been called once. Called 0 times.

Starsze wersje Pythona pozwalają na ciche przekazywanie błędnie napisanych metod asercji, takich jak assret_called_once. Nowoczesne wersje zgłaszają błąd w przypadku nazw zaczynających się na assert lub w przypadku typowych błędów pisowni, ale specyfikacja jest nadal silniejszą ochroną.

pytest i kpi

Wszystko powyżej działa również w pyteście. Wielu użytkowników pytest używa także wbudowanego urządzenia monkeypatch do prostych zamian lub wtyczki pytest-mock, która udostępnia urządzenie mocker, które automatycznie cofa poprawki. Zasada „łatka tam, gdzie jest używana” dotyczy ich wszystkich. Zobacz nasz samouczek pytest dotyczący osprzętu i konfiguracji.

Kiedy nie kpić

  • Nie kpij z tego, co testujesz. Jeśli kpina wykonuje całą pracę, test niczego nie dowodzi.
  • Unikaj drwin z prostych obiektów wartościowych. Używaj rzeczy prawdziwych.
  • Nie kpij ze wszystkiego. Testy pełne prób często psują się po refaktoryzacji kodu, nawet jeśli zachowanie jest prawidłowe.
  • Przeprowadź kilka prawdziwych testów integracyjnych. Próby kodują Twoje założenia dotyczące usługi, a te założenia mogą być błędne.

Próby również łatwo zwiększają pokrycie kodu, ponieważ wyśmiewane ścieżki działają bez żadnego prawdziwego zachowania.

Typowe błędy w szyderstwie

  • Poprawienie niewłaściwej nazwy. Omówione powyżej, ale nadal główna przyczyna „zignorowania mojej kpiny”.
  • Zapominanie o ustawieniu wartości zwracanej. Testowany kod zamiast danych otrzymuje obiekt Mock. Może przejść bezpośrednio przez twoją funkcję i przez przypadek przejść test, na przykład wtedy, gdy sprawdzasz tylko, czy wynik jest prawdziwy.
  • Twierdzenie na podstawie próby zamiast wyniku. Sprawdzenie, czy wywołano save(), jest słabsze niż sprawdzenie, co zostało zapisane. Użyj assert_called_once_with(...) z rzeczywistymi argumentami lub lepiej, potwierdź wynik funkcji.
  • Wyciekające łatki. Wywołanie patcher.start() bez stop() pozostawia podróbkę na miejscu do późniejszych testów. Użyj menedżera kontekstu, dekoratora lub addCleanup(patcher.stop).
  • Dekoratory ułożone w niewłaściwej kolejności. W przypadku kilku @patch dekoratorów, jako pierwszy argument przekazywany jest dolny. Mieszanie ich po cichu konfiguruje niewłaściwą próbę.
  • Wyśmiewanie biblioteki, której nie jesteś właścicielem w wielu testach. Zawiń to w małą, własną klasę i zamiast tego kpij z niej. Twoje testy opisują następnie Twoje potrzeby, a nie interfejs API biblioteki.

Kpiny, skróty i podróbki

Słowa te są często używane luźno, ale różnice są pomocne przy omawianiu testów. odcinek zwraca po prostu odpowiedzi w puszkach. próba również rejestruje rozmowy, dzięki czemu możesz sprawdzić, w jaki sposób została wykorzystana. fałszywa to działająca, uproszczona implementacja, taka jak repozytorium w pamięci zamiast bazy danych. Python Mock może działać jako kod pośredniczący lub próbny. Fake to zwykłe klasy, które sam piszesz i często prowadzą do testów, które lepiej wytrzymują refaktoryzację, ponieważ sprawdzają zachowanie, a nie dokładne wywołania.

Dobrą zasadą jest kpienie z brzegów systemu: sieci, zegara, liczb losowych, systemu plików i płatnych usług stron trzecich. Wewnątrz tych krawędzi preferuj prawdziwe przedmioty. Testy zbudowane w ten sposób działają szybko i nadal wykorzystują faktycznie napisany kod.

Podsumowanie

Szydzenie to precyzyjne narzędzie: zastępuj powolne lub nieprzewidywalne zależności, kontroluj, co zwracają i sprawdzaj, jak Twój kod je wykorzystał. Dwa nawyki zapobiegają większości problemów. Popraw nazwę w testowanym module i użyj autospecowania, aby Twoje próbne wywołania odrzucały wywołania, które odrzuciłby prawdziwy obiekt.

Najczęstsze pytania

Co to jest kpina w Pythonie?

Mocking powoduje zastąpienie prawdziwego obiektu, takiego jak klient API lub baza danych, podróbką, którą kontrolujesz podczas testu. Python uwzględnia to w module unittest.mock.

Jaka jest różnica między Mockiem a MagicMockiem?

MagicMock to Mock, który obsługuje także metody magiczne, takie jak __len__, __iter__ i __enter__. patch() domyślnie tworzy MagicMock.

Dlaczego moja poprawka nie działa?

Zwykle dlatego, że poprawiłeś miejsce, w którym obiekt jest zdefiniowany, a nie miejsce, w którym jest używany. Popraw nazwę wewnątrz testowanego modułu.

Jaka jest różnica między wartością zwracaną a efektem ubocznym?

return_value ustawia stały wynik. side_effect może zgłosić wyjątek, zwrócić wartości z listy w określonej kolejności lub wywołać funkcję.

Co robi autospec?

Tworzy makietę z prawdziwego obiektu, więc brakujące atrybuty i błędne argumenty powodują błędy, tak samo jak w przypadku prawdziwego kodu.

Ucz się dalej

Pracuj z Stack Quora

Potrzebujesz pomocy przy testowaniu, WordPressie lub SEO?

Pomagamy zespołom dostarczać lepsze oprogramowanie i rozwijać ich witryny. Powiedz nam, nad czym pracujesz, a my zaproponujemy jasny następny krok.

Czytaj dalej

Więcej poradników na testowanie, sztuczna inteligencja i nowoczesny rozwój

Podobał Ci się ten artykuł? Wybierz temat i przejdź bezpośrednio do naszych najnowszych tutoriali.

Sztuczna inteligencja

Wyjaśnienie halucynacji AI: dlaczego modele językowe tworzą rzeczy i jak je zmniejszyć

Wyjaśniono halucynacje AI: czym są, dlaczego powstają w modelach językowych, typowe typy, jak je zredukować za pomocą uziemień i podpowiedzi oraz pr…

Przeczytaj artykuł →
DevOps

Wyjaśnienie zmiennych środowiskowych: pliki .env, sekrety i konfiguracja wykonana prawidłowo

Zmienne środowiskowe wyjaśnione na przykładach Pythona, które przeprowadziliśmy: ich odczytywanie i ustawianie, dlaczego każda wartość jest ciągiem …

Przeczytaj artykuł →
Tworzenie oprogramowania

Wyjaśnienie ograniczenia szybkości interfejsu API: algorytmy, nagłówki i obsługa błędów 429

Ograniczanie szybkości API wyjaśnione za pomocą przeprowadzonej przez nas symulacji: zasobnik tokenów, stałe okno, przesuwane okno, 429 odpowiedzi, …

Przeczytaj artykuł →
Automatyzacja testów

Wyśmiewanie w Pythonie: unittest.mock, patch i błędy, które sprawiają, że testy kłamią

Wyśmiewanie w Pythonie za pomocą unittest.mock, przetestowane: Mock i zwracana wartość, efekt uboczny, łatanie tam, gdzie używana jest nazwa, specyf…

Przeczytaj artykuł →
Testowanie oprogramowania

Wyjaśnienie zasięgu kodu: pokrycie linii i oddziału oraz co naprawdę oznacza 100%

Wyjaśnienie pokrycia kodu za pomocą prawdziwego pliku cover.py: pokrycie linii, gałęzi i warunków, przeczytanie raportu, dlaczego 100% pokrycia może…

Przeczytaj artykuł →
Bazy danych i SQL

DELETE vs TRUNCATE vs DROP w SQL: różnice, wycofywanie i kiedy używać każdego z nich

DELETE vs TRUNCATE vs DROP wyjaśnione za pomocą przetestowanego SQL: co każdy usuwa, wycofywanie, wyzwalacze, resetowanie tożsamości, prędkość i czy…

Przeczytaj artykuł →
Bazy danych i SQL

Wyjaśnienie widoków SQL: tworzenie, używanie i unikanie pułapek wraz z przykładami

Widoki SQL wyjaśnione za pomocą zapytań, które uruchomiliśmy: tworzenie widoku, wyniki na żywo, ukrywanie kolumn, wydajność, widoki z możliwością ak…

Przeczytaj artykuł →
Tworzenie oprogramowania

Interfejs TypeScript a typ: rzeczywiste różnice, testowane za pomocą kompilatora

Interfejs TypeScript a typ, sprawdzony z prawdziwym kompilatorem: co oba mogą zrobić, co tylko jeden, łączenie deklaracji, rozszerzenia a przecięcia…

Przeczytaj artykuł →
Tworzenie oprogramowania

Python *args i **kwargs wyjaśnione: elastyczne funkcje z przejrzystymi przykładami

Python *args i **kwargs wyjaśnione za pomocą uruchomionego przez nas kodu: pakowanie i rozpakowywanie, kolejność parametrów, argumenty zawierające t…

Przeczytaj artykuł →
Tworzenie oprogramowania

Funkcje Lambda w Pythonie: składnia, rzeczywiste zastosowania i kiedy ich nie używać

Funkcje lambda w Pythonie wyjaśnione za pomocą uruchomionego przez nas kodu: składnia, sortowanie za pomocą klucza, mapa i filtr, domknięcia, pułapk…

Przeczytaj artykuł →
Sztuczna inteligencja

Kim są agenci AI? Jak one działają, dzięki prostej pętli agenta, którą możesz uruchomić

Kim są agenci AI? Prosty przewodnik po pętli agenta, narzędziach, pamięci i poręczach, z małą pętlą w Pythonie, którą uruchomiliśmy, prawdziwymi zag…

Przeczytaj artykuł →
DevOps

Monitorowanie a obserwowalność: prawdziwa różnica z logami, wskaźnikami i śladami

Wyjaśnienie monitorowania a obserwowalności: znane i nieznane awarie, dzienniki, metryki i ślady, opis incydentu przy kasie, złote sygnały, OpenTele…

Przeczytaj artykuł →
DevOps

Woluminy platformy Docker a mocowania powiązań: utrwalanie danych kontenera we właściwy sposób

Wyjaśniono woluminy Dockera a montowanie powiązań: dlaczego dane kontenera znikają, nazwane woluminy, montowanie powiązań, tmpfs, przykłady tworzeni…

Przeczytaj artykuł →
Automatyzacja testów

TDD vs BDD: kluczowe różnice, jedna funkcja zbudowana w obie strony i kiedy używać każdej z nich

TDD vs BDD wyjaśnione za pomocą jednej funkcji rabatowej zbudowanej w obie strony: refaktoryzacja czerwono-zielona z uruchomionym przez nas kodem, s…

Przeczytaj artykuł →
Tworzenie oprogramowania

Git merge vs rebase: różnice, przykłady i kiedy czego używać

Git merge vs rebase wyjaśnione na prawdziwym wyniku logu, z tabelą porównawczą, złotą zasadą rebase i bezpiecznym procesem pracy zespołu.

Przeczytaj artykuł →
Tworzenie oprogramowania

Python list comprehension: składnia, przykłady i kiedy jej unikać

Python list comprehension wyjaśnione: składnia, filtrowanie, if-else, zagnieżdżone pętle, zmierzone czasy i sytuacje, w których zwykła pętla for jes…

Przeczytaj artykuł →
Bazy danych i SQL

SQL JOIN – wyjaśnienie z przykładami (Inner, Left, Right, Full)

Zrozum złączenia SQL krok po kroku na przykładowych tabelach i zapytaniach: INNER, LEFT, RIGHT, FULL, CROSS i SELF, plus typowe błędy i wskazówki wy…

Przeczytaj artykuł →