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.
- mock zastępuje prawdziwy obiekt, taki jak klient API, więc testy przebiegają szybko i w trybie offline.
- Użyj
return_valuedla ustalonej odpowiedzi iside_effectdla 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
speclubautospec, 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żyjassert_called_once_with(...)z rzeczywistymi argumentami lub lepiej, potwierdź wynik funkcji. - Wyciekające łatki. Wywołanie
patcher.start()bezstop()pozostawia podróbkę na miejscu do późniejszych testów. Użyj menedżera kontekstu, dekoratora lubaddCleanup(patcher.stop). - Dekoratory ułożone w niewłaściwej kolejności. W przypadku kilku
@patchdekoratoró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
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.
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.
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ł →DevOpsWyjaś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 oprogramowaniaWyjaś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ówWyś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 oprogramowaniaWyjaś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 SQLDELETE 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 SQLWyjaś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 oprogramowaniaInterfejs 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 oprogramowaniaPython *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 oprogramowaniaFunkcje 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 inteligencjaKim 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ł →DevOpsMonitorowanie 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ł →DevOpsWoluminy 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ówTDD 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 oprogramowaniaGit 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 oprogramowaniaPython 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 SQLSQL 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ł →