Objevte testování založené na vlastnostech s knihovnou Hypothesis v Pythonu. Posuňte se od testů založených na příkladech k hledání okrajových případů a budování robustnějšího a spolehlivějšího softwaru.
Nad rámec unit testů: Hloubkový ponor do testování založeného na vlastnostech s Python's Hypothesis
Ve světě vývoje softwaru je testování základem kvality. Po celá desetiletí byla dominantním paradigmatem testování založené na příkladech. Pečlivě vytváříme vstupy, definujeme očekávané výstupy a píšeme aserce, abychom ověřili, že se náš kód chová podle plánu. Tento přístup, který se nachází v rámcích jako unittest
a pytest
, je silný a zásadní. Ale co kdybych vám řekl, že existuje doplňkový přístup, který může odhalit chyby, o kterých vás ani nenapadlo přemýšlet?
Vítejte ve světě testování založeného na vlastnostech, paradigmatu, které přesouvá pozornost od testování konkrétních příkladů k ověřování obecných vlastností vašeho kódu. A v ekosystému Pythonu je nesporným šampionem tohoto přístupu knihovna s názvem Hypothesis.
Tento komplexní průvodce vás provede od naprostého začátečníka až po sebevědomého praktika testování založeného na vlastnostech s Hypothesis. Prozkoumáme základní koncepty, ponoříme se do praktických příkladů a naučíme se, jak integrovat tento výkonný nástroj do vašeho každodenního vývojového pracovního postupu, abychom vytvořili robustnější, spolehlivější a chybám odolnější software.
Co je testování založené na vlastnostech? Změna myšlení
Abychom pochopili Hypothesis, musíme nejprve pochopit základní myšlenku testování založeného na vlastnostech. Porovnejme to s tradičním testováním založeným na příkladech, které všichni známe.
Testování založené na příkladech: Známá cesta
Představte si, že jste napsali vlastní funkci řazení, my_sort()
. S testováním založeným na příkladech by váš myšlenkový proces byl:
- "Otestujme to s jednoduchým, uspořádaným seznamem." ->
assert my_sort([1, 2, 3]) == [1, 2, 3]
- "Co takhle seznam s obráceným pořadím?" ->
assert my_sort([3, 2, 1]) == [1, 2, 3]
- "Co takhle prázdný seznam?" ->
assert my_sort([]) == []
- "Seznam s duplikáty?" ->
assert my_sort([5, 1, 5, 2]) == [1, 2, 5, 5]
- "A seznam se zápornými čísly?" ->
assert my_sort([-1, -5, 0]) == [-5, -1, 0]
To je efektivní, ale má to zásadní omezení: testujete pouze případy, které vás napadnou. Vaše testy jsou pouze tak dobré, jak je vaše představivost. Můžete vynechat okrajové případy zahrnující velmi velká čísla, nepřesnosti v plovoucí řádové čárce, specifické znaky unicode nebo složité kombinace dat, které vedou k neočekávanému chování.
Testování založené na vlastnostech: Přemýšlení v invariantách
Testování založené na vlastnostech obrací scénář. Místo poskytování konkrétních příkladů definujete vlastnosti nebo invarianty vaší funkce – pravidla, která by měla platit pro jakýkoli platný vstup. Pro naši funkci my_sort()
by tyto vlastnosti mohly být:
- Výstup je seřazen: Pro jakýkoli seznam čísel je každý prvek ve výstupním seznamu menší nebo roven tomu, který následuje.
- Výstup obsahuje stejné prvky jako vstup: Seřazený seznam je pouze permutací původního seznamu; žádné prvky se nepřidávají ani neztrácejí.
- Funkce je idempotentní: Seřazení již seřazeného seznamu by se nemělo změnit. To znamená, že
my_sort(my_sort(some_list)) == my_sort(some_list)
.
S tímto přístupem nepíšete testovací data. Píšete pravidla. Poté necháte rámec, jako je Hypothesis, vygenerovat stovky nebo tisíce náhodných, rozmanitých a často lstivých vstupů, aby se pokusil prokázat, že vaše vlastnosti jsou špatné. Pokud najde vstup, který porušuje vlastnost, našel chybu.
Představujeme Hypothesis: Váš automatizovaný generátor testovacích dat
Hypothesis je přední knihovna pro testování založeného na vlastnostech pro Python. Bere vlastnosti, které definujete, a dělá těžkou práci generování testovacích dat, aby je napadla. Není to jen generátor náhodných dat; je to inteligentní a výkonný nástroj navržený tak, aby efektivně hledal chyby.
Klíčové vlastnosti Hypothesis
- Automatické generování testovacích případů: Definujete *tvar* dat, která potřebujete (např. "seznam celých čísel", "řetězec obsahující pouze písmena", "datum a čas v budoucnu") a Hypothesis generuje širokou škálu příkladů, které odpovídají tomuto tvaru.
- Inteligentní zmenšování: Toto je kouzelná funkce. Když Hypothesis najde selhávající testovací případ (např. seznam 50 komplexních čísel, který zhroutí vaši funkci řazení), neoznámí pouze tento masivní seznam. Inteligentně a automaticky zjednodušuje vstup, aby našla nejmenší možný příklad, který stále způsobuje selhání. Místo 50prvkového seznamu může ohlásit, že k selhání dochází pouze s
[inf, nan]
. Díky tomu je ladění neuvěřitelně rychlé a efektivní. - Bezproblémová integrace: Hypothesis se perfektně integruje s populárními testovacími rámci jako
pytest
aunittest
. Můžete přidat testy založené na vlastnostech vedle stávajících testů založených na příkladech, aniž byste změnili svůj pracovní postup. - Bohatá knihovna strategií: Dodává se s rozsáhlou sbírkou vestavěných "strategií" pro generování všeho od jednoduchých celých čísel a řetězců po složité, vnořené datové struktury, časově citlivé datetimes a dokonce i pole NumPy.
- Testování se stavem: U složitějších systémů může Hypothesis testovat sekvence akcí, aby našla chyby ve stavových přechodech, což je něco, co je notoricky obtížné s testováním založeným na příkladech.
Začínáme: Váš první test Hypothesis
Pojďme si ušpinit ruce. Nejlepší způsob, jak pochopit Hypothesis, je vidět to v akci.
Instalace
Nejprve budete muset nainstalovat Hypothesis a váš testovací nástroj dle výběru (použijeme pytest
). Je to tak jednoduché jako:
pip install pytest hypothesis
Jednoduchý příklad: Funkce absolutní hodnoty
Podívejme se na jednoduchou funkci, která má vypočítat absolutní hodnotu čísla. Mírně chybná implementace by mohla vypadat takto:
# v souboru s názvem `my_math.py` def custom_abs(x): """Vlastní implementace funkce absolutní hodnoty.""" if x < 0: return -x return x
Nyní pojďme napsat testovací soubor, test_my_math.py
. Nejprve tradiční přístup pytest
:
# test_my_math.py (Založeno na příkladu) def test_abs_positive(): assert custom_abs(5) == 5 def test_abs_negative(): assert custom_abs(-5) == 5 def test_abs_zero(): assert custom_abs(0) == 0
Tyto testy procházejí. Naše funkce vypadá správně na základě těchto příkladů. Ale teď pojďme napsat test založený na vlastnostech s Hypothesis. Jaká je základní vlastnost funkce absolutní hodnoty? Výsledek by nikdy neměl být záporný.
# test_my_math.py (Založeno na vlastnosti s Hypothesis) from hypothesis import given from hypothesis import strategies as st from my_math import custom_abs @given(st.integers()) def test_abs_property_is_non_negative(x): """Vlastnost: Absolutní hodnota jakéhokoli celého čísla je vždy >= 0.""" assert custom_abs(x) >= 0
Rozdělme to:
from hypothesis import given, strategies as st
: Importujeme potřebné komponenty.given
je dekorátor, který změní běžnou testovací funkci na test založený na vlastnostech.strategies
je modul, kde najdeme naše generátory dat.@given(st.integers())
: Toto je jádro testu. Dekorátor@given
říká Hypothesis, aby spustil tuto testovací funkci několikrát. Pro každé spuštění vygeneruje hodnotu pomocí poskytnuté strategiest.integers()
a předá ji jako argumentx
naší testovací funkci.assert custom_abs(x) >= 0
: Toto je naše vlastnost. Tvrdíme, že pro jakékoli celé číslox
, které si Hypothesis vysní, musí být výsledek naší funkce větší nebo roven nule.
Když to spustíte s pytest
, pravděpodobně to projde pro mnoho hodnot. Hypothesis se pokusí 0, -1, 1, velká kladná čísla, velká záporná čísla a další. Naše jednoduchá funkce to všechno zvládá správně. Nyní zkusme jinou strategii, abychom zjistili, zda můžeme najít slabinu.
# Otestujme s čísly s plovoucí řádovou čárkou @given(st.floats()) def test_abs_floats_property(x): assert custom_abs(x) >= 0
Pokud to spustíte, Hypothesis rychle najde selhávající případ!
Falsifying example: test_abs_floats_property(x=nan) ... assert custom_abs(nan) >= 0 AssertionError: assert nan >= 0
Hypothesis zjistil, že naše funkce při zadání float('nan')
(Not a Number) vrací nan
. Asert nan >= 0
je false. Právě jsme našli jemnou chybu, kterou bychom pravděpodobně nenapadlo testovat ručně. Mohli bychom opravit naši funkci tak, aby to zvládla, možná vyvoláním ValueError
nebo vrácením konkrétní hodnoty.
Ještě lépe, co když byla chyba s velmi specifickým floatem? Hypothesis's shrinker by vzal velké, komplexní selhávající číslo a zredukoval ho na nejjednodušší možnou verzi, která stále spouští chybu.
Síla strategií: Vytváření vašich testovacích dat
Strategie jsou srdcem Hypothesis. Jsou to recepty na generování dat. Knihovna obsahuje rozsáhlé pole vestavěných strategií a můžete je kombinovat a přizpůsobovat tak, aby generovaly prakticky jakoukoli datovou strukturu, kterou si dokážete představit.
Běžné vestavěné strategie
- Numerické:
st.integers(min_value=0, max_value=1000)
: Generuje celá čísla, volitelně v určitém rozsahu.st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
: Generuje floats, s jemně odstupňovanou kontrolou nad speciálními hodnotami.st.fractions()
,st.decimals()
- Text:
st.text(min_size=1, max_size=50)
: Generuje řetězce unicode určité délky.st.text(alphabet='abcdef0123456789')
: Generuje řetězce ze specifické sady znaků (např. pro hex kódy).st.characters()
: Generuje jednotlivé znaky.
- Kolekce:
st.lists(st.integers(), min_size=1)
: Generuje seznamy, kde je každý prvek celé číslo. Všimněte si, jak předáváme jinou strategii jako argument! Tomu se říká kompozice.st.tuples(st.text(), st.booleans())
: Generuje n-tice s pevnou strukturou.st.sets(st.integers())
st.dictionaries(keys=st.text(), values=st.integers())
: Generuje slovníky se zadanými typy klíčů a hodnot.
- Časové:
st.dates()
,st.times()
,st.datetimes()
,st.timedeltas()
. Ty lze vytvořit s povědomím o časovém pásmu.
- Různé:
st.booleans()
: GenerujeTrue
neboFalse
.st.just('constant_value')
: Vždy generuje stejnou jedinou hodnotu. Užitečné pro skládání složitých strategií.st.one_of(st.integers(), st.text())
: Generuje hodnotu z jedné z poskytnutých strategií.st.none()
: Generuje pouzeNone
.
Kombinování a transformace strategií
Skutečná síla Hypothesis pochází z jeho schopnosti vytvářet složité strategie z jednodušších.
Použití .map()
Metoda .map()
umožňuje vzít hodnotu z jedné strategie a transformovat ji na něco jiného. To je ideální pro vytváření objektů vašich vlastních tříd.
# Jednoduchá datová třída from dataclasses import dataclass @dataclass class User: user_id: int username: str # Strategie pro generování objektů User user_strategy = st.builds( User, user_id=st.integers(min_value=1), username=st.text(min_size=3, alphabet='abcdefghijklmnopqrstuvwxyz') ) @given(user=user_strategy) def test_user_creation(user): assert isinstance(user, User) assert user.user_id > 0 assert user.username.isalpha()
Použití .filter()
a assume()
Někdy je třeba odmítnout určité generované hodnoty. Například možná budete potřebovat seznam celých čísel, kde součet není nula. Můžete použít .filter()
:
st.lists(st.integers()).filter(lambda x: sum(x) != 0)
Použití .filter()
však může být neefektivní. Pokud je podmínka často false, může Hypothesis strávit spoustu času pokusem vygenerovat platný příklad. Lepší přístup je často použít assume()
uvnitř vaší testovací funkce:
from hypothesis import assume @given(st.lists(st.integers())) def test_something_with_non_zero_sum_list(numbers): assume(sum(numbers) != 0) # ... vaše testovací logika zde ...
assume()
říká Hypothesis: "Pokud tato podmínka není splněna, stačí tento příklad zahodit a zkusit nový." Je to přímější a často výkonnější způsob, jak omezit testovací data.
Použití st.composite()
Pro skutečně složité generování dat, kde jedna generovaná hodnota závisí na druhé, je st.composite()
nástroj, který potřebujete. Umožňuje vám napsat funkci, která jako argument přijímá speciální funkci draw
, kterou můžete použít k postupnému vytahování hodnot z jiných strategií.
Klasickým příkladem je generování seznamu a platného indexu do tohoto seznamu.
@st.composite def list_and_index(draw): # Nejprve nakreslete neprázdný seznam my_list = draw(st.lists(st.integers(), min_size=1)) # Poté nakreslete index, který je zaručeně platný pro tento seznam index = draw(st.integers(min_value=0, max_value=len(my_list) - 1)) return (my_list, index) @given(data=list_and_index()) def test_list_access(data): my_list, index = data # Tento přístup je zaručeně bezpečný díky tomu, jak jsme vytvořili strategii element = my_list[index] assert element is not None # Jednoduché tvrzení
Hypothesis v akci: Scénáře z reálného světa
Aplikujme tyto koncepty na realističtější problémy, kterým vývojáři softwaru čelí každý den.
Scénář 1: Testování funkce serializace dat
Představte si funkci, která serializuje uživatelský profil (slovník) do řetězce bezpečného pro URL a další, která jej deserializuje. Klíčovou vlastností je, že proces by měl být dokonale reverzibilní.
import json import base64 def serialize_profile(data: dict) -> str: """Serializuje slovník na řetězec base64 bezpečný pro URL.""" json_string = json.dumps(data) return base64.urlsafe_b64encode(json_string.encode('utf-8')).decode('utf-8') def deserialize_profile(encoded_str: str) -> dict: """Deseriazuje řetězec zpět do slovníku.""" json_string = base64.urlsafe_b64decode(encoded_str.encode('utf-8')).decode('utf-8') return json.loads(json_string) # Nyní pro test # Potřebujeme strategii, která generuje slovníky kompatibilní s JSON json_dictionaries = st.dictionaries( keys=st.text(), values=st.recursive(st.none() | st.booleans() | st.floats(allow_nan=False) | st.text(), lambda children: st.lists(children) | st.dictionaries(st.text(), children), max_leaves=10) ) @given(profile=json_dictionaries) def test_serialization_roundtrip(profile): """Vlastnost: Deserializace kódovaného profilu by měla vrátit původní profil.""" encoded = serialize_profile(profile) decoded = deserialize_profile(encoded) assert profile == decoded
Tento jediný test zatluče naše funkce s masivní škálou dat: prázdné slovníky, slovníky s vnořenými seznamy, slovníky se znaky unicode, slovníky s podivnými klíči a další. Je to mnohem důkladnější než psaní několika manuálních příkladů.
Scénář 2: Testování řadicího algoritmu
Pojďme se znovu podívat na náš příklad řazení. Zde je způsob, jak otestovat vlastnosti, které jsme definovali dříve.
from collections import Counter def my_buggy_sort(numbers): # Pojďme zavést jemnou chybu: zahodí duplikáty return sorted(list(set(numbers))) @given(st.lists(st.integers())) def test_sorting_properties(numbers): sorted_list = my_buggy_sort(numbers) # Vlastnost 1: Výstup je seřazen for i in range(len(sorted_list) - 1): assert sorted_list[i] <= sorted_list[i+1] # Vlastnost 2: Prvky jsou stejné (to najde chybu) assert Counter(numbers) == Counter(sorted_list) # Vlastnost 3: Funkce je idempotentní assert my_buggy_sort(sorted_list) == sorted_list
Když spustíte tento test, Hypothesis rychle najde selhávající příklad pro vlastnost 2, například numbers=[0, 0]
. Naše funkce vrací [0]
a Counter([0, 0])
se nerovná Counter([0])
. Shrnovač zajistí, že selhávající příklad bude co nejjednodušší, což způsobí, že příčina chyby bude okamžitě zřejmá.
Scénář 3: Testování se stavem
Pro objekty s interním stavem, který se časem mění (jako databázové připojení, nákupní košík nebo mezipaměť), může být hledání chyb neuvěřitelně obtížné. K aktivaci chyby může být vyžadována konkrétní sekvence operací. Hypothesis poskytuje `RuleBasedStateMachine` přesně za tímto účelem.
Představte si jednoduché API pro klíč-hodnota úložiště v paměti:
class SimpleKeyValueStore: def __init__(self): self._data = {} def set(self, key, value): self._data[key] = value def get(self, key): return self._data.get(key) def delete(self, key): if key in self._data: del self._data[key] def size(self): return len(self._data)
Můžeme modelovat jeho chování a testovat jej pomocí stavového automatu:
from hypothesis.stateful import RuleBasedStateMachine, rule, Bundle class KeyValueStoreMachine(RuleBasedStateMachine): def __init__(self): super().__init__() self.model = {} self.sut = SimpleKeyValueStore() # Bundle() se používá k předávání dat mezi pravidly keys = Bundle('keys') @rule(target=keys, key=st.text(), value=st.integers()) def set_key(self, key, value): self.model[key] = value self.sut.set(key, value) return key @rule(key=keys) def delete_key(self, key): del self.model[key] self.sut.delete(key) @rule(key=st.text()) def get_key(self, key): model_val = self.model.get(key) sut_val = self.sut.get(key) assert model_val == sut_val @rule() def check_size(self): assert len(self.model) == self.sut.size() # Pro spuštění testu stačí podtřídit ze stroje a unittest.TestCase # V pytestu můžete test jednoduše přiřadit ke třídě stroje TestKeyValueStore = KeyValueStoreMachine.TestCase
Hypothesis nyní spustí náhodné sekvence operací `set_key`, `delete_key`, `get_key` a `check_size` a neúnavně se pokusí najít sekvenci, která způsobí selhání jednoho z tvrzení. Zkontroluje, zda se získání smazaného klíče chová správně, zda je velikost konzistentní po více nastaveních a smazáních a mnoho dalších scénářů, které by vás možná nenapadlo testovat ručně.
Osvědčené postupy a pokročilé tipy
- Databáze příkladů: Hypothesis je chytrý. Když najde chybu, uloží selhávající příklad do lokálního adresáře (
.hypothesis/
). Při příštím spuštění testů nejprve přehraje tento selhávající příklad a poskytne vám okamžitou zpětnou vazbu, že chyba stále existuje. Jakmile to opravíte, příklad se již nepřehrává. - Ovládání provádění testu pomocí
@settings
: Můžete ovládat mnoho aspektů spuštění testu pomocí dekorátoru@settings
. Můžete zvýšit počet příkladů, nastavit termín, jak dlouho může trvat jeden příklad (k zachycení nekonečných smyček), a vypnout určité kontroly stavu.@settings(max_examples=500, deadline=1000) # Spusťte 500 příkladů, termín 1 sekunda @given(...) ...
- Reprodukce selhání: Každé spuštění Hypothesis tiskne hodnotu seed (např.
@reproduce_failure('version', 'seed')
). Pokud server CI najde chybu, kterou nelze lokálně reprodukovat, můžete použít tento dekorátor s poskytnutým semenem, aby se Hypothesis nutil spustit přesně stejnou sekvenci příkladů. - Integrace s CI/CD: Hypothesis se perfektně hodí pro jakýkoli kanál kontinuální integrace. Jeho schopnost najít nejasné chyby dříve, než se dostanou do produkce, z něj činí neocenitelnou záchrannou síť.
Změna myšlení: Přemýšlení ve vlastnostech
Přijetí Hypothesis je víc než jen naučit se novou knihovnu; jde o přijetí nového způsobu myšlení o správnosti vašeho kódu. Místo toho, abyste se ptali: "Jaké vstupy bych měl testovat?", začnete se ptát: "Jaké jsou univerzální pravdy o tomto kódu?"Zde je několik otázek, které vás povedou při pokusu o identifikaci vlastností:
- Existuje reverzní operace? (např. serializace/deserializace, šifrování/dešifrování, komprese/dekomprese). Vlastností je, že provedení operace a její reverze by mělo přinést původní vstup.
- Je operace idempotentní? (např.
abs(abs(x)) == abs(x)
). Opakované použití funkce by mělo přinést stejný výsledek jako její jednorázové použití. - Existuje jiný, jednodušší způsob, jak vypočítat stejný výsledek? Můžete otestovat, že vaše složitá, optimalizovaná funkce produkuje stejný výstup jako jednoduchá, samozřejmě správná verze (např. testování vašeho efektního řazení proti vestavěnému
sorted()
Pythonu). - Co by mělo být o výstupu vždy pravdivé? (např. výstup funkce `find_prime_factors` by měl obsahovat pouze prvočísla a jejich součin by se měl rovnat vstupu).
- Jak se stav mění? (Pro testování se stavem) Jaké invarianty je třeba zachovat po jakékoli platné operaci? (např. Počet položek v nákupním košíku nemůže být nikdy záporný).
Závěr: Nová úroveň důvěry
Testování založené na vlastnostech s Hypothesis nenahrazuje testování založené na příkladech. Stále potřebujete specifické, ručně psané testy pro kritickou obchodní logiku a dobře srozumitelné požadavky (např. "Uživatel ze země X musí vidět cenu Y").
To, co Hypothesis poskytuje, je výkonný, automatizovaný způsob, jak prozkoumat chování vašeho kódu a chránit se před nepředvídatelnými okrajovými případy. Působí jako neúnavný partner, generuje tisíce testů, které jsou rozmanitější a lstivější, než by je mohl reálně napsat jakýkoli člověk. Definujete-li základní vlastnosti svého kódu, vytvoříte robustní specifikaci, proti které může Hypothesis testovat, což vám dává novou úroveň důvěry ve svůj software.
Až příště napíšete funkci, věnujte chvilku přemýšlení nad rámec příkladů. Zeptejte se sami sebe: "Jaká jsou pravidla? Co musí být vždy pravda?" Pak nechte Hypothesis, aby se ujala těžké práce a pokusila se je porušit. Budete překvapeni tím, co najde, a váš kód bude díky tomu lepší.