Odkrijte testiranje na osnovi lastnosti s Pythonovo knjižnico Hypothesis. Premaknite se od testov na osnovi primerov k iskanju robnih primerov in ustvarjanju bolj robustne, zanesljive programske opreme.
Onkraj enotnih testov: Poglobljen vpogled v testiranje na osnovi lastnosti s Pythonovo knjižnico Hypothesis
V svetu razvoja programske opreme je testiranje temelj kakovosti. Že desetletja je prevladujoča paradigma testiranje na osnovi primerov. Skrbno oblikujemo vhode, definiramo pričakovane izhode in pišemo preverjanja, da zagotovimo, da naša koda deluje po načrtih. Ta pristop, ki ga najdemo v ogrodjih, kot sta unittest
in pytest
, je močan in bistven. Kaj pa, če bi vam povedal, da obstaja dopolnilni pristop, ki lahko odkrije napake, ki jih nikoli niste pomislili iskati?
Dobrodošli v svetu testiranja na osnovi lastnosti, paradigme, ki premakne fokus s testiranja specifičnih primerov na preverjanje splošnih lastnosti vaše kode. In v ekosistemu Python je nesporni prvak tega pristopa knjižnica z imenom Hypothesis.
Ta obsežen vodnik vas bo popeljal od popolnega začetnika do samozavestnega praktika testiranja na osnovi lastnosti s knjižnico Hypothesis. Raziskali bomo temeljne koncepte, se poglobili v praktične primere in se naučili, kako to močno orodje integrirati v vaš vsakodnevni razvojni potek dela za ustvarjanje bolj robustne, zanesljive in proti napakam odporne programske opreme.
Kaj je testiranje na osnovi lastnosti? Premik v miselnosti
Da bi razumeli knjižnico Hypothesis, moramo najprej razumeti temeljno idejo testiranja na osnovi lastnosti. Primerjajmo ga s tradicionalnim testiranjem na osnovi primerov, ki ga vsi poznamo.
Testiranje na osnovi primerov: Znana pot
Predstavljajte si, da ste napisali funkcijo za sortiranje po meri, my_sort()
. Pri testiranju na osnovi primerov bi vaš miselni proces potekal takole:
- »Preizkusimo jo s preprostim urejenim seznamom.« ->
assert my_sort([1, 2, 3]) == [1, 2, 3]
- »Kaj pa obrnjen seznam?« ->
assert my_sort([3, 2, 1]) == [1, 2, 3]
- »Kaj pa prazen seznam?« ->
assert my_sort([]) == []
- »Seznam s podvojenimi elementi?« ->
assert my_sort([5, 1, 5, 2]) == [1, 2, 5, 5]
- »In seznam z negativnimi števili?« ->
assert my_sort([-1, -5, 0]) == [-5, -1, 0]
To je učinkovito, vendar ima temeljno omejitev: testirate samo primere, na katere lahko pomislite. Vaši testi so le tako dobri kot vaša domišljija. Morda boste zamudili robne primere, ki vključujejo zelo velika števila, natančnost plavajoče vejice, specifične unicode znake ali zapletene kombinacije podatkov, ki vodijo do nepričakovanega vedenja.
Testiranje na osnovi lastnosti: Razmišljanje o invariantah
Testiranje na osnovi lastnosti obrne vrstni red. Namesto da bi zagotovili specifične primere, definirate lastnosti ali invarinante vaše funkcije – pravila, ki naj bi veljala za kakršn koli veljaven vnos. Za našo funkcijo my_sort()
bi te lastnosti lahko bile:
- Izhod je urejen: Za kateri koli seznam števil je vsak element v izhodnem seznamu manjši ali enak tistemu, ki mu sledi.
- Izhod vsebuje iste elemente kot vhod: Urejen seznam je le permutacija izvirnega seznama; nobeni elementi niso dodani ali izgubljeni.
- Funkcija je idempotentna: Sortiranje že urejenega seznama ga ne sme spremeniti. To pomeni, da
my_sort(my_sort(nek_seznam)) == my_sort(nek_seznam)
.
S tem pristopom ne pišete testnih podatkov. Pišete pravila. Nato pustite, da ogrodje, kot je Hypothesis, ustvari na stotine ali tisoče naključnih, raznolikih in pogosto zvijačnih vhodov, da poskusi dokazati, da vaše lastnosti niso pravilne. Če najde vnos, ki krši lastnost, je našel napako.
Predstavljamo knjižnico Hypothesis: Vaš avtomatizirani generator testnih podatkov
Hypothesis je vodilna knjižnica za testiranje na osnovi lastnosti za Python. Vzame lastnosti, ki jih definirate, in opravi težko delo generiranja testnih podatkov za njihovo izpodbijanje. To ni le naključen generator podatkov; je inteligentno in močno orodje, zasnovano za učinkovito iskanje napak.
Ključne lastnosti knjižnice Hypothesis
- Samodejno generiranje testnih primerov: Definirate *obliko* podatkov, ki jih potrebujete (npr. »seznam celih števil«, »niz, ki vsebuje samo črke«, »datum in čas v prihodnosti«), in Hypothesis ustvari široko paleto primerov, ki ustrezajo tej obliki.
- Inteligentno krčenje: To je čarobna lastnost. Ko Hypothesis najde primer, ki ne uspe (npr. seznam 50 kompleksnih števil, ki zruši vašo funkcijo sortiranja), ne prijavi samo tega ogromnega seznama. Inteligentno in samodejno poenostavi vnos, da najde najmanjši možni primer, ki še vedno povzroča napako. Namesto seznama s 50 elementi morda prijavi, da se napaka pojavi samo z
[inf, nan]
. To naredi odpravljanje napak neverjetno hitro in učinkovito. - Brezhibna integracija: Hypothesis se popolnoma integrira s priljubljenimi ogrodji za testiranje, kot sta
pytest
inunittest
. Testne primere na osnovi lastnosti lahko dodate poleg vaših obstoječih testnih primerov na osnovi primerov, ne da bi spremenili svoj potek dela. - Bogata knjižnica strategij: Vključuje obsežno zbirko vgrajenih »strategij« za generiranje vsega, od preprostih celih števil in nizov do zapletenih, vgnezdjenih podatkovnih struktur, datumov in časov z zavedanjem časovnega pasu in celo NumPy polj.
- Testiranje stanj: Za bolj zapletene sisteme lahko Hypothesis testira zaporedja dejanj, da bi našel napake v prehodih stanj, kar je z testiranjem na osnovi primerov izjemno težko.
Začetek: Vaš prvi test s knjižnico Hypothesis
Umazajmo si roke. Najboljši način, da razumete knjižnico Hypothesis, je, da jo vidite v akciji.
Namestitev
Najprej boste morali namestiti knjižnico Hypothesis in izbrani izvajalnik testov (uporabili bomo pytest
). To je tako preprosto kot:
pip install pytest hypothesis
Preprost primer: Funkcija absolutne vrednosti
Razmislimo o preprosti funkciji, ki naj bi izračunala absolutno vrednost števila. Nekoliko napačna implementacija bi lahko izgledala takole:
# v datoteki z imenom `my_math.py` def custom_abs(x): """Funkcija po meri za izračun absolutne vrednosti.""" if x < 0: return -x return x
Zdaj pa napišimo testno datoteko, test_my_math.py
. Najprej tradicionalni pristop pytest
:
# test_my_math.py (Testiranje na osnovi primerov) 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
Ti testi so uspešni. Naša funkcija se na podlagi teh primerov zdi pravilna. Zdaj pa napišimo test na osnovi lastnosti s knjižnico Hypothesis. Kakšna je temeljna lastnost funkcije absolutne vrednosti? Rezultat nikoli ne sme biti negativen.
# test_my_math.py (Testiranje na osnovi lastnosti s knjižnico 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): """Lastnost: Absolutna vrednost katerega koli celega števila je vedno >= 0.""" assert custom_abs(x) >= 0
Razčlenimo to:
from hypothesis import given, strategies as st
: Uvozimo potrebne komponente.given
je okrasitelj, ki redno testno funkcijo spremeni v test na osnovi lastnosti.strategies
je modul, kjer najdemo naše generatorje podatkov.@given(st.integers())
: To je jedro testa. Okrasitelj@given
pove knjižnici Hypothesis, naj to testno funkcijo zažene večkrat. Za vsak zagon bo ustvarila vrednost z uporabo priložene strategijest.integers()
in jo kot argumentx
posredovala naši testni funkciji.assert custom_abs(x) >= 0
: To je naša lastnost. Preverimo, da mora za katero koli celo številox
, ki si ga knjižnica Hypothesis izmisli, rezultat naše funkcije biti večji ali enak nič.
Ko to zaženete s pytest
, bo verjetno uspelo pri mnogih vrednostih. Knjižnica Hypothesis bo poskusila 0, -1, 1, velika pozitivna števila, velika negativna števila in še več. Naša preprosta funkcija vse to pravilno obravnava. Zdaj pa poskusimo drugačno strategijo, da vidimo, ali lahko najdemo šibkost.
# Poskusimo s števili s plavajočo vejico @given(st.floats()) def test_abs_floats_property(x): assert custom_abs(x) >= 0
Če to zaženete, bo knjižnica Hypothesis hitro našla primer napake!
Primer, ki ga ne uspemo najti: test_abs_floats_property(x=nan) ... assert custom_abs(nan) >= 0 AssertionError: assert nan >= 0
Knjižnica Hypothesis je odkrila, da naša funkcija vrne nan
, ko dobi float('nan')
(Not a Number). Preverjanje nan >= 0
je napačno. Pravkar smo našli subtilno napako, za katero verjetno ne bi pomislili, da bi jo preizkusili ročno. Svojo funkcijo bi lahko popravili, da bi obravnavala ta primer, morda z dvigom ValueError
ali vračilom določene vrednosti.
Še bolje, kaj če bi bila napaka pri zelo specifičnem plavajočem številu? Krčitelj knjižnice Hypothesis bi vzel veliko, zapleteno neuspelo število in ga zmanjšal na najpreprostejši možni primer, ki še vedno povzroča napako.
Moč strategij: Priprava vaših testnih podatkov
Strategije so srce knjižnice Hypothesis. So recepti za generiranje podatkov. Knjižnica vključuje ogromno paleto vgrajenih strategij, jih lahko kombinirate in prilagajate, da ustvarite skoraj katero koli podatkovno strukturo, ki si jo lahko zamislite.
Pogoste vgrajene strategije
- Številske:
st.integers(min_value=0, max_value=1000)
: Generira cela števila, opcijsko v določenem območju.st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
: Generira plavajoča števila z natančnim nadzorom nad posebnimi vrednostmi.st.fractions()
,st.decimals()
- Besedilo:
st.text(min_size=1, max_size=50)
: Generira unicode nize določene dolžine.st.text(alphabet='abcdef0123456789')
: Generira nize iz določenega nabora znakov (npr. za heksadecimalne kode).st.characters()
: Generira posamezne znake.
- Zbirke:
st.lists(st.integers(), min_size=1)
: Generira sezname, kjer je vsak element celo število. Opazite, kako posredujemo drugo strategijo kot argument! To se imenuje kompozicija.st.tuples(st.text(), st.booleans())
: Generira nize z določeno strukturo.st.sets(st.integers())
st.dictionaries(keys=st.text(), values=st.integers())
: Generira slovarje z določenimi vrstami ključev in vrednosti.
- Časovne:
st.dates()
,st.times()
,st.datetimes()
,st.timedeltas()
. Te je mogoče narediti zavedne o časovnem pasu.
- Razno:
st.booleans()
: GeneriraTrue
aliFalse
.st.just('constant_value')
: Vedno generira isto samostojno vrednost. Uporabno za sestavljanje kompleksnih strategij.st.one_of(st.integers(), st.text())
: Generira vrednost iz ene od priloženih strategij.st.none()
: Generira samoNone
.
Kombiniranje in transformiranje strategij
Resnična moč knjižnice Hypothesis izvira iz njene zmožnosti gradnje kompleksnih strategij iz enostavnejših.
Uporaba .map()
Metoda .map()
vam omogoča, da vzamete vrednost iz ene strategije in jo transformirate v nekaj drugega. To je kot nalašč za ustvarjanje objektov vaših lastnih razredov.
# Preprost razred podatkov from dataclasses import dataclass @dataclass class User: user_id: int username: str # Strategija za generiranje objektov 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()
Uporaba .filter()
in assume()
Včasih morate zavrniti nekatere generirane vrednosti. Na primer, morda potrebujete seznam celih števil, katerih vsota ni nič. Uporabili bi lahko .filter()
:
st.lists(st.integers()).filter(lambda x: sum(x) != 0)
Vendar lahko uporaba .filter()
povzroči neučinkovitost. Če je pogoj pogosto napačen, lahko knjižnica Hypothesis porabi veliko časa za generiranje veljavnega primera. Pogosto je boljši pristop uporaba assume()
znotraj vaše testne funkcije:
from hypothesis import assume @given(st.lists(st.integers())) def test_something_with_non_zero_sum_list(numbers): assume(sum(numbers) != 0) # ... vaša testna logika tukaj ...
assume()
pove knjižnici Hypothesis: »Če ta pogoj ni izpolnjen, samo zavrzi ta primer in poskusi novega.« To je bolj neposreden in pogosto učinkovitejši način za omejevanje vaših testnih podatkov.
Uporaba st.composite()
Za resnično kompleksno generiranje podatkov, kjer ena generirana vrednost temelji na drugi, je st.composite()
orodje, ki ga potrebujete. Omogoča vam, da napišete funkcijo, ki kot argument vzame poseben draw
funkcijo, ki jo lahko uporabite za pridobivanje vrednosti iz drugih strategij korak za korakom.
Klasičen primer je generiranje seznama in veljavnega indeksa v tem seznamu.
@st.composite def list_and_index(draw): # Najprej izvlečemo ne-prazen seznam my_list = draw(st.lists(st.integers(), min_size=1)) # Nato izvlečemo indeks, ki je zagotovljeno veljaven za ta 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 # Ta dostop je zagotovljeno varen zaradi načina, kako smo zgradili strategijo element = my_list[index] assert element is not None # Preprosto preverjanje
Hypothesis v akciji: Realni scenariji
Te koncepte bomo uporabili pri bolj realističnih problemih, s katerimi se razvijalci programske opreme srečujejo vsak dan.
Scenarij 1: Testiranje funkcije za serializacijo podatkov
Predstavljajte si funkcijo, ki serializira uporabniški profil (slovar) v niz, varen za URL, in drugo, ki ga deserializira. Ključna lastnost je, da mora biti postopek popolnoma obrnljiv.
import json import base64 def serialize_profile(data: dict) -> str: """Serializira slovar v URL-varen base64 niz.""" json_string = json.dumps(data) return base64.urlsafe_b64encode(json_string.encode('utf-8')).decode('utf-8') def deserialize_profile(encoded_str: str) -> dict: """Deserializira niz nazaj v slovar.""" json_string = base64.urlsafe_b64decode(encoded_str.encode('utf-8')).decode('utf-8') return json.loads(json_string) # Zdaj za test # Potrebujemo strategijo, ki generira slovarje, združljive z 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): """Lastnost: Deserializacija kodiranega profila naj vrne izvirni profil.""" encoded = serialize_profile(profile) decoded = deserialize_profile(encoded) assert profile == decoded
Ta en sam test bo naše funkcije preobremenil z ogromno različnimi podatki: prazni slovarji, slovarji z vnezdnjenimi seznami, slovarji z unicode znaki, slovarji s čudnimi ključi in še več. Je veliko temeljitejši kot pisanje nekaj ročnih primerov.
Scenarij 2: Testiranje algoritma za sortiranje
Ponovno se vrnimo k našemu primeru sortiranja. Tukaj je, kako bi testirali lastnosti, ki smo jih prej definirali.
from collections import Counter def my_buggy_sort(numbers): # Vnesimo subtilno napako: izgubi podvojene elemente return sorted(list(set(numbers))) @given(st.lists(st.integers())) def test_sorting_properties(numbers): sorted_list = my_buggy_sort(numbers) # Lastnost 1: Izhod je urejen for i in range(len(sorted_list) - 1): assert sorted_list[i] <= sorted_list[i+1] # Lastnost 2: Elementi so isti (to bo našlo napako) assert Counter(numbers) == Counter(sorted_list) # Lastnost 3: Funkcija je idempotentna assert my_buggy_sort(sorted_list) == sorted_list
Ko zaženete ta test, bo knjižnica Hypothesis hitro našla neuspeli primer za Lastnost 2, na primer numbers=[0, 0]
. Naša funkcija vrne [0]
, in Counter([0, 0])
ni enako Counter([0])
. Krčitelj bo zagotovil, da je neuspeli primer čim bolj preprost, zaradi česar bo vzrok napake takoj očiten.
Scenarij 3: Testiranje stanj
Za objekte z notranjim stanjem, ki se s časom spreminja (kot je povezava z bazo podatkov, nakupovalni voziček ali predpomnilnik), je iskanje napak lahko izjemno težko. Posebno zaporedje operacij je lahko potrebno za sprožitev napake. Knjižnica Hypothesis zagotavlja `RuleBasedStateMachine` natančno za ta namen.
Predstavljajte si preprosto API za ključ-vrednost shrambo v pomnilniku:
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)
Njeno vedenje lahko modeliramo in jo testiramo s strojem stanj:
from hypothesis.stateful import RuleBasedStateMachine, rule, Bundle class KeyValueStoreMachine(RuleBasedStateMachine): def __init__(self): super().__init__() self.model = {} self.sut = SimpleKeyValueStore() # Bundle() se uporablja za posredovanje podatkov med pravili 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() # Za zagon testa se preprosto razširite iz stroja in unittest.TestCase # V pytest-u lahko test preprosto dodelite razredu stroja TestKeyValueStore = KeyValueStoreMachine.TestCase
Knjižnica Hypothesis bo zdaj izvajala naključna zaporedja operacij `set_key`, `delete_key`, `get_key` in `check_size`, neusmiljeno poskušala najti zaporedje, ki povzroči, da katera od preverjanj ne uspe. Preverila bo, ali pravilno pridobivanje izbrisanega ključa obnašanje, ali je velikost skladna po več nastavitvah in brisanjih ter številne druge scenarije, ki jih morda ne bi testirali ročno.
Najboljše prakse in napredni nasveti
- Podatkovna baza primerov: Knjižnica Hypothesis je pametna. Ko najde napako, shrani primer, ki ni uspel, v lokalno mapo (
.hypothesis/
). Naslednjič, ko zaženete teste, bo predvajala ta primer, ki ni uspel, kar vam bo takoj sporočilo, da je napaka še vedno prisotna. Ko jo popravite, se primer ne predvaja več. - Nadzor izvajanja testov z
@settings
: Veliko vidikov izvajanja testov lahko nadzirate z okrasiteljem@settings
. Število primerov lahko povečate, nastavite rok, kako dolgo lahko traja en primer (da ujamete neskončne zanke), in izklopite nekatere zdravstvene preglede.@settings(max_examples=500, deadline=1000) # Zaženi 500 primerov, rok 1 sekunda @given(...) ...
- Ponovitev neuspelih primerov: Vsak zagon knjižnice Hypothesis natisne vrednost semena (npr.
@reproduce_failure('version', 'seed')
). Če strežnik CI najde napako, ki je ne morete reproducirati lokalno, lahko z okrasiteljem, ki je priložen s semenom, prisilite knjižnico Hypothesis, da izvede popolnoma enako zaporedje primerov. - Integracija z CI/CD: Knjižnica Hypothesis je popolnoma primerna za kateri koli pipeline neprekinjene integracije. Njena sposobnost iskanja nejasnih napak, preden dosežejo produkcijo, jo naredi neprecenljivo varnostno mrežo.
Premik v miselnosti: Razmišljanje o lastnostih
Sprejetje knjižnice Hypothesis je več kot le učenje nove knjižnice; gre za sprejemanje novega načina razmišljanja o pravilnosti vaše kode. Namesto da bi vprašali »Katere vnose naj testiram?«, se začnete spraševati »Katere so univerzalne resnice o tej kodi?«
Tukaj je nekaj vprašanj, ki vas vodijo pri poskusu prepoznavanja lastnosti:
- Obstaja povratna operacija? (npr. serializacija/deserializacija, šifriranje/dešifriranje, kompresija/dekompresija). Lastnost je, da izvajanje operacije in njene povratne operacije vrne izvirni vnos.
- Je operacija idempotentna? (npr.
abs(abs(x)) == abs(x)
). Večkratno izvajanje funkcije naj proizvede enak rezultat kot enkratno izvajanje. - Obstaja drugačen, enostavnejši način za izračun istega rezultata? Lahko testirate, da vaša zapletena, optimizirana funkcija proizvede enak izhod kot preprosta, očitno pravilna različica (npr. testiranje vašega naprednega sortiranja s Pythonovim vgrajenim
sorted()
). - Kaj mora vedno veljati za izhod? (npr. izhod funkcije `find_prime_factors` naj vsebuje samo praštevila, njun produkt pa naj bo enak vhodu).
- Kako se stanje spreminja? (Za testiranje stanj) Katere invarinante je treba ohraniti po kateri koli veljavni operaciji? (npr. Število elementov v nakupovalnem vozičku ne sme biti nikoli negativno).
Zaključek: Nova raven zaupanja
Testiranje na osnovi lastnosti s knjižnico Hypothesis ne nadomešča testiranja na osnovi primerov. Še vedno potrebujete specifične, ročno napisane teste za kritično poslovno logiko in dobro razumljene zahteve (npr. »Uporabnik iz države X mora videti ceno Y«).
Kar knjižnica Hypothesis ponuja, je močan, avtomatiziran način za raziskovanje vedenja vaše kode in varovanje pred nepričakovanimi robnimi primeri. Deluje kot neusmiljen partner, ki ustvarja tisoče testov, ki so bolj raznoliki in zvijačni, kot bi jih kateri koli človek lahko realno napisal. Z definiranjem temeljnih lastnosti vaše kode ustvarite robustno specifikacijo, proti kateri lahko knjižnica Hypothesis testira, kar vam daje novo raven zaupanja v vašo programsko opremo.
Naslednjič, ko boste napisali funkcijo, si vzemite trenutek in razmislite onkraj primerov. Vprašajte se: »Kakšna so pravila? Kaj mora vedno veljati?« Nato pustite knjižnici Hypothesis, da opravi težko delo poskusa, da jih zlomi. Presenečeni boste nad tem, kaj najde, in vaša koda bo zaradi tega boljša.