Ανακαλύψτε το property-based testing με τη βιβλιοθήκη Hypothesis της Python. Ξεπεράστε τις δοκιμές βάσει παραδειγμάτων για να βρείτε ακραίες περιπτώσεις και να δημιουργήσετε πιο ανθεκτικό, αξιόπιστο λογισμικό.
Πέρα από τα Unit Tests: Μια Εις Βάθος Ανάλυση του Property-Based Testing με το Hypothesis της Python
Στον κόσμο της ανάπτυξης λογισμικού, οι δοκιμές αποτελούν το θεμέλιο της ποιότητας. Για δεκαετίες, το κυρίαρχο παράδειγμα ήταν το testing βάσει παραδειγμάτων. Κατασκευάζουμε σχολαστικά εισόδους, ορίζουμε τις αναμενόμενες εξόδους και γράφουμε δηλώσεις για να επαληθεύσουμε ότι ο κώδικάς μας λειτουργεί όπως είχε προγραμματιστεί. Αυτή η προσέγγιση, που βρίσκεται σε frameworks όπως το unittest
και το pytest
, είναι ισχυρή και απαραίτητη. Τι θα λέγατε όμως αν σας έλεγα ότι υπάρχει μια συμπληρωματική προσέγγιση που μπορεί να αποκαλύψει σφάλματα που ποτέ δεν σκεφτήκατε να αναζητήσετε;
Καλώς ήρθατε στον κόσμο του property-based testing, ενός παραδείγματος που μετατοπίζει την εστίαση από τη δοκιμή συγκεκριμένων παραδειγμάτων στην επαλήθευση γενικών ιδιοτήτων του κώδικά σας. Και στο οικοσύστημα της Python, ο αδιαμφισβήτητος πρωταθλητής αυτής της προσέγγισης είναι μια βιβλιοθήκη που ονομάζεται Hypothesis.
Αυτός ο περιεκτικός οδηγός θα σας μετατρέψει από έναν εντελώς αρχάριο σε έναν πεπειραμένο επαγγελματία του property-based testing με το Hypothesis. Θα εξερευνήσουμε τις βασικές έννοιες, θα εμβαθύνουμε σε πρακτικά παραδείγματα και θα μάθουμε πώς να ενσωματώσετε αυτό το ισχυρό εργαλείο στην καθημερινή σας ροή εργασίας ανάπτυξης για να δημιουργήσετε πιο ανθεκτικό, αξιόπιστο και ανθεκτικό σε σφάλματα λογισμικό.
Τι είναι το Property-Based Testing; Μια Αλλαγή Νοοτροπίας
Για να κατανοήσουμε το Hypothesis, πρέπει πρώτα να κατανοήσουμε τη θεμελιώδη ιδέα του property-based testing. Ας το συγκρίνουμε με το παραδοσιακό testing βάσει παραδειγμάτων που όλοι γνωρίζουμε.
Testing βάσει Παραδειγμάτων: Η Οικεία Διαδρομή
Φανταστείτε ότι έχετε γράψει μια προσαρμοσμένη συνάρτηση ταξινόμησης, την my_sort()
. Με το testing βάσει παραδειγμάτων, η διαδικασία σκέψης σας θα ήταν:
- "Ας το δοκιμάσουμε με μια απλή, ταξινομημένη λίστα." ->
assert my_sort([1, 2, 3]) == [1, 2, 3]
- "Τι γίνεται με μια αντίστροφα ταξινομημένη λίστα;" ->
assert my_sort([3, 2, 1]) == [1, 2, 3]
- "Τι γίνεται με μια κενή λίστα;" ->
assert my_sort([]) == []
- "Μια λίστα με διπλότυπα;" ->
assert my_sort([5, 1, 5, 2]) == [1, 2, 5, 5]
- "Και μια λίστα με αρνητικούς αριθμούς;" ->
assert my_sort([-1, -5, 0]) == [-5, -1, 0]
Αυτό είναι αποτελεσματικό, αλλά έχει έναν θεμελιώδη περιορισμό: δοκιμάζετε μόνο τις περιπτώσεις που μπορείτε να σκεφτείτε. Οι δοκιμές σας είναι τόσο καλές όσο και η φαντασία σας. Μπορεί να χάσετε ακραίες περιπτώσεις που αφορούν πολύ μεγάλους αριθμούς, ανακρίβειες κινητής υποδιαστολής, συγκεκριμένους χαρακτήρες unicode ή πολύπλοκους συνδυασμούς δεδομένων που οδηγούν σε απρόβλεπτη συμπεριφορά.
Property-Based Testing: Σκέψη σε Αμετάβλητα
Το property-based testing αντιστρέφει το σενάριο. Αντί να παρέχετε συγκεκριμένα παραδείγματα, ορίζετε τις ιδιότητες, ή αμετάβλητα, της συνάρτησής σας—κανόνες που θα πρέπει να ισχύουν για οποιαδήποτε έγκυρη είσοδο. Για τη συνάρτηση my_sort()
, αυτές οι ιδιότητες μπορεί να είναι:
- Η έξοδος είναι ταξινομημένη: Για οποιαδήποτε λίστα αριθμών, κάθε στοιχείο στην τελική λίστα είναι μικρότερο ή ίσο με το επόμενο.
- Η έξοδος περιέχει τα ίδια στοιχεία με την είσοδο: Η ταξινομημένη λίστα είναι απλώς μια αναδιάταξη της αρχικής λίστας· δεν προστίθενται ή χάνονται στοιχεία.
- Η συνάρτηση είναι ιδεοδύναμη: Η ταξινόμηση μιας ήδη ταξινομημένης λίστας δεν πρέπει να την αλλάζει. Δηλαδή,
my_sort(my_sort(some_list)) == my_sort(some_list)
.
Με αυτή την προσέγγιση, δεν γράφετε τα δεδομένα δοκιμής. Γράφετε τους κανόνες. Στη συνέχεια, αφήνετε ένα framework, όπως το Hypothesis, να δημιουργήσει εκατοντάδες ή χιλιάδες τυχαίες, ποικίλες και συχνά πανούργες εισόδους για να προσπαθήσει να αποδείξει ότι οι ιδιότητές σας είναι λανθασμένες. Αν βρει μια είσοδο που παραβιάζει μια ιδιότητα, έχει βρει ένα σφάλμα.
Παρουσιάζοντας το Hypothesis: Ο Αυτοματοποιημένος Δημιουργός Δεδομένων Δοκιμής σας
Το Hypothesis είναι η κορυφαία βιβλιοθήκη property-based testing για την Python. Παίρνει τις ιδιότητες που ορίζετε και κάνει τη δύσκολη δουλειά της δημιουργίας δεδομένων δοκιμής για να τις αμφισβητήσει. Δεν είναι απλώς ένας τυχαίος γεννήτορας δεδομένων· είναι ένα έξυπνο και ισχυρό εργαλείο σχεδιασμένο να βρίσκει σφάλματα αποτελεσματικά.
Βασικά Χαρακτηριστικά του Hypothesis
- Αυτόματη Δημιουργία Περιπτώσεων Δοκιμής: Ορίζετε το *σχήμα* των δεδομένων που χρειάζεστε (π.χ., "μια λίστα ακεραίων", "μια συμβολοσειρά που περιέχει μόνο γράμματα", "μια ημερομηνία/ώρα στο μέλλον"), και το Hypothesis δημιουργεί μια μεγάλη ποικιλία παραδειγμάτων που συμμορφώνονται με αυτό το σχήμα.
- Έξυπνη Συρρίκνωση (Shrinking): Αυτό είναι το μαγικό χαρακτηριστικό. Όταν το Hypothesis βρίσκει μια αποτυχημένη περίπτωση δοκιμής (π.χ., μια λίστα 50 μιγαδικών αριθμών που καταρρίπτει τη συνάρτηση ταξινόμησής σας), δεν αναφέρει απλώς αυτή την τεράστια λίστα. Απλοποιεί έξυπνα και αυτόματα την είσοδο για να βρει το μικρότερο δυνατό παράδειγμα που εξακολουθεί να προκαλεί την αποτυχία. Αντί για μια λίστα 50 στοιχείων, μπορεί να αναφέρει ότι η αποτυχία συμβαίνει με απλώς
[inf, nan]
. Αυτό καθιστά το debugging απίστευτα γρήγορο και αποτελεσματικό. - Απρόσκοπτη Ενσωμάτωση: Το Hypothesis ενσωματώνεται τέλεια με δημοφιλή frameworks δοκιμών όπως το
pytest
και τοunittest
. Μπορείτε να προσθέσετε δοκιμές βάσει ιδιοτήτων παράλληλα με τις υπάρχουσες δοκιμές βάσει παραδειγμάτων χωρίς να αλλάξετε τη ροή εργασίας σας. - Πλούσια Βιβλιοθήκη Στρατηγικών: Έρχεται με μια τεράστια συλλογή ενσωματωμένων "στρατηγικών" για τη δημιουργία οτιδήποτε, από απλούς ακέραιους αριθμούς και συμβολοσειρές έως σύνθετες, ένθετες δομές δεδομένων, datetimes με επίγνωση ζώνης ώρας, ακόμη και πίνακες NumPy.
- Stateful Testing: Για πιο πολύπλοκα συστήματα, το Hypothesis μπορεί να δοκιμάσει ακολουθίες ενεργειών για να βρει σφάλματα σε μεταβάσεις κατάστασης, κάτι που είναι διαβόητα δύσκολο με το testing βάσει παραδειγμάτων.
Ξεκινώντας: Η Πρώτη σας Δοκιμή Hypothesis
Ας πιάσουμε δουλειά. Ο καλύτερος τρόπος να κατανοήσετε το Hypothesis είναι να το δείτε σε δράση.
Εγκατάσταση
Πρώτα, θα χρειαστεί να εγκαταστήσετε το Hypothesis και τον test runner της επιλογής σας (θα χρησιμοποιήσουμε το pytest
). Είναι τόσο απλό όσο:
pip install pytest hypothesis
Ένα Απλό Παράδειγμα: Μια Συνάρτηση Απόλυτης Τιμής
Ας εξετάσουμε μια απλή συνάρτηση που υποτίθεται ότι υπολογίζει την απόλυτη τιμή ενός αριθμού. Μια ελαφρώς ελαττωματική υλοποίηση μπορεί να μοιάζει ως εξής:
# in a file named `my_math.py` def custom_abs(x): """A custom implementation of the absolute value function.""" if x < 0: return -x return x
Τώρα, ας γράψουμε ένα αρχείο δοκιμών, το test_my_math.py
. Πρώτα, η παραδοσιακή προσέγγιση pytest
:
# test_my_math.py (Example-based) 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
Αυτές οι δοκιμές περνούν. Η συνάρτησή μας φαίνεται σωστή βάσει αυτών των παραδειγμάτων. Αλλά τώρα, ας γράψουμε μια δοκιμή βάσει ιδιοτήτων με το Hypothesis. Ποια είναι μια βασική ιδιότητα της συνάρτησης απόλυτης τιμής; Το αποτέλεσμα δεν πρέπει ποτέ να είναι αρνητικό.
# test_my_math.py (Property-based with 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): """Property: The absolute value of any integer is always >= 0.""" assert custom_abs(x) >= 0
Ας το αναλύσουμε:
from hypothesis import given, strategies as st
: Εισάγουμε τα απαραίτητα συστατικά. Τοgiven
είναι ένας decorator που μετατρέπει μια κανονική συνάρτηση δοκιμής σε δοκιμή βάσει ιδιοτήτων. Τοstrategies
είναι η ενότητα όπου βρίσκουμε τους γεννήτορες δεδομένων μας.@given(st.integers())
: Αυτός είναι ο πυρήνας της δοκιμής. Ο decorator@given
λέει στο Hypothesis να εκτελέσει αυτή τη συνάρτηση δοκιμής πολλές φορές. Για κάθε εκτέλεση, θα δημιουργήσει μια τιμή χρησιμοποιώντας την παρεχόμενη στρατηγική,st.integers()
, και θα την περάσει ως όρισμαx
στη συνάρτηση δοκιμής μας.assert custom_abs(x) >= 0
: Αυτή είναι η ιδιότητά μας. Δηλώνουμε ότι για οποιονδήποτε ακέραιοx
το Hypothesis φανταστεί, το αποτέλεσμα της συνάρτησής μας πρέπει να είναι μεγαλύτερο ή ίσο του μηδενός.
Όταν το εκτελέσετε με το pytest
, πιθανότατα θα περάσει για πολλές τιμές. Το Hypothesis θα δοκιμάσει 0, -1, 1, μεγάλους θετικούς αριθμούς, μεγάλους αρνητικούς αριθμούς και άλλα. Η απλή συνάρτησή μας χειρίζεται όλα αυτά σωστά. Τώρα, ας δοκιμάσουμε μια διαφορετική στρατηγική για να δούμε αν μπορούμε να βρούμε μια αδυναμία.
# Let's test with floating point numbers @given(st.floats()) def test_abs_floats_property(x): assert custom_abs(x) >= 0
Αν το εκτελέσετε, το Hypothesis θα βρει γρήγορα μια αποτυχημένη περίπτωση!
Falsifying example: test_abs_floats_property(x=nan) ... assert custom_abs(nan) >= 0 AssertionError: assert nan >= 0
Το Hypothesis ανακάλυψε ότι η συνάρτησή μας, όταν της δίνεται float('nan')
(Όχι Αριθμός), επιστρέφει nan
. Η δήλωση nan >= 0
είναι ψευδής. Μόλις βρήκαμε ένα λεπτό σφάλμα που πιθανότατα δεν θα είχαμε σκεφτεί να δοκιμάσουμε χειροκίνητα. Θα μπορούσαμε να διορθώσουμε τη συνάρτησή μας για να χειριστούμε αυτή την περίπτωση, ίσως με την εμφάνιση ενός ValueError
ή την επιστροφή μιας συγκεκριμένης τιμής.
Ακόμα καλύτερα, τι θα γινόταν αν το σφάλμα ήταν με έναν πολύ συγκεκριμένο float; Ο συρρικνωτής του Hypothesis θα είχε πάρει έναν μεγάλο, πολύπλοκο αριθμό που αποτυγχάνει και θα τον είχε μειώσει στην απλούστερη δυνατή έκδοση που εξακολουθεί να προκαλεί το σφάλμα.
Η Δύναμη των Στρατηγικών: Δημιουργώντας τα Δεδομένα Δοκιμής σας
Οι στρατηγικές είναι η καρδιά του Hypothesis. Είναι συνταγές για τη δημιουργία δεδομένων. Η βιβλιοθήκη περιλαμβάνει μια τεράστια σειρά ενσωματωμένων στρατηγικών, και μπορείτε να τις συνδυάσετε και να τις προσαρμόσετε για να δημιουργήσετε σχεδόν οποιαδήποτε δομή δεδομένων μπορείτε να φανταστείτε.
Κοινές Ενσωματωμένες Στρατηγικές
- Αριθμητικές:
st.integers(min_value=0, max_value=1000)
: Δημιουργεί ακέραιους, προαιρετικά εντός συγκεκριμένου εύρους.st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
: Δημιουργεί αριθμούς κινητής υποδιαστολής (floats), με λεπτομερή έλεγχο ειδικών τιμών.st.fractions()
,st.decimals()
- Κείμενο:
st.text(min_size=1, max_size=50)
: Δημιουργεί συμβολοσειρές unicode συγκεκριμένου μήκους.st.text(alphabet='abcdef0123456789')
: Δημιουργεί συμβολοσειρές από ένα συγκεκριμένο σύνολο χαρακτήρων (π.χ., για δεκαεξαδικούς κωδικούς).st.characters()
: Δημιουργεί μεμονωμένους χαρακτήρες.
- Συλλογές:
st.lists(st.integers(), min_size=1)
: Δημιουργεί λίστες όπου κάθε στοιχείο είναι ακέραιος αριθμός. Παρατηρήστε πώς περνάμε μια άλλη στρατηγική ως όρισμα! Αυτό ονομάζεται σύνθεση.st.tuples(st.text(), st.booleans())
: Δημιουργεί πλειάδες (tuples) με σταθερή δομή.st.sets(st.integers())
st.dictionaries(keys=st.text(), values=st.integers())
: Δημιουργεί λεξικά με καθορισμένους τύπους κλειδιών και τιμών.
- Χρονικές:
st.dates()
,st.times()
,st.datetimes()
,st.timedeltas()
. Αυτές μπορούν να γίνουν με επίγνωση ζώνης ώρας.
- Διάφορες:
st.booleans()
: ΔημιουργείTrue
ήFalse
.st.just('constant_value')
: Πάντα δημιουργεί την ίδια μοναδική τιμή. Χρήσιμο για τη σύνθεση πολύπλοκων στρατηγικών.st.one_of(st.integers(), st.text())
: Δημιουργεί μια τιμή από μία από τις παρεχόμενες στρατηγικές.st.none()
: Δημιουργεί μόνοNone
.
Συνδυάζοντας και Μετασχηματίζοντας Στρατηγικές
Η πραγματική δύναμη του Hypothesis προέρχεται από την ικανότητά του να χτίζει πολύπλοκες στρατηγικές από απλούστερες.
Χρήση του .map()
Η μέθοδος .map()
σάς επιτρέπει να πάρετε μια τιμή από μια στρατηγική και να την μετατρέψετε σε κάτι άλλο. Αυτό είναι ιδανικό για τη δημιουργία αντικειμένων των προσαρμοσμένων κλάσεων σας.
# A simple data class from dataclasses import dataclass @dataclass class User: user_id: int username: str # A strategy to generate User objects 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()
Χρήση των .filter()
και assume()
Μερικές φορές πρέπει να απορρίψετε ορισμένες παραγόμενες τιμές. Για παράδειγμα, μπορεί να χρειάζεστε μια λίστα ακεραίων όπου το άθροισμα δεν είναι μηδέν. Θα μπορούσατε να χρησιμοποιήσετε το .filter()
:
st.lists(st.integers()).filter(lambda x: sum(x) != 0)
Ωστόσο, η χρήση του .filter()
μπορεί να είναι αναποτελεσματική. Εάν η συνθήκη είναι συχνά ψευδής, το Hypothesis μπορεί να αφιερώσει πολύ χρόνο προσπαθώντας να δημιουργήσει ένα έγκυρο παράδειγμα. Μια καλύτερη προσέγγιση είναι συχνά να χρησιμοποιείτε το assume()
μέσα στη συνάρτηση δοκιμής σας:
from hypothesis import assume @given(st.lists(st.integers())) def test_something_with_non_zero_sum_list(numbers): assume(sum(numbers) != 0) # ... your test logic here ...
Το assume()
λέει στο Hypothesis: "Εάν αυτή η συνθήκη δεν πληρούται, απλώς απορρίψτε αυτό το παράδειγμα και δοκιμάστε ένα νέο." Είναι ένας πιο άμεσος και συχνά πιο αποδοτικός τρόπος να περιορίσετε τα δεδομένα δοκιμής σας.
Χρήση του st.composite()
Για πραγματικά πολύπλοκη δημιουργία δεδομένων, όπου μια παραγόμενη τιμή εξαρτάται από μια άλλη, το st.composite()
είναι το εργαλείο που χρειάζεστε. Σας επιτρέπει να γράψετε μια συνάρτηση που δέχεται μια ειδική συνάρτηση draw
ως όρισμα, την οποία μπορείτε να χρησιμοποιήσετε για να αντλήσετε τιμές από άλλες στρατηγικές βήμα προς βήμα.
Ένα κλασικό παράδειγμα είναι η δημιουργία μιας λίστας και ενός έγκυρου δείκτη σε αυτή τη λίστα.
@st.composite def list_and_index(draw): # First, draw a non-empty list my_list = draw(st.lists(st.integers(), min_size=1)) # Then, draw an index that is guaranteed to be valid for that list 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 # This access is guaranteed to be safe because of how we built the strategy element = my_list[index] assert element is not None # A simple assertion
Hypothesis σε Δράση: Σενάρια Πραγματικού Κόσμου
Ας εφαρμόσουμε αυτές τις έννοιες σε πιο ρεαλιστικά προβλήματα που αντιμετωπίζουν καθημερινά οι προγραμματιστές λογισμικού.
Σενάριο 1: Δοκιμή μιας Συνάρτησης Σειριοποίησης Δεδομένων
Φανταστείτε μια συνάρτηση που σειριοποιεί ένα προφίλ χρήστη (ένα λεξικό) σε μια URL-safe συμβολοσειρά και μια άλλη που την αποσειριοποιεί. Μια βασική ιδιότητα είναι ότι η διαδικασία πρέπει να είναι πλήρως αντιστρέψιμη.
import json import base64 def serialize_profile(data: dict) -> str: """Serializes a dictionary to a URL-safe base64 string.""" json_string = json.dumps(data) return base64.urlsafe_b64encode(json_string.encode('utf-8')).decode('utf-8') def deserialize_profile(encoded_str: str) -> dict: """Deserializes a string back into a dictionary.""" json_string = base64.urlsafe_b64decode(encoded_str.encode('utf-8')).decode('utf-8') return json.loads(json_string) # Now for the test # We need a strategy that generates JSON-compatible dictionaries 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): """Property: Deserializing an encoded profile should return the original profile.""" encoded = serialize_profile(profile) decoded = deserialize_profile(encoded) assert profile == decoded
Αυτή η μία δοκιμή θα "σφυροκοπήσει" τις συναρτήσεις μας με μια τεράστια ποικιλία δεδομένων: κενά λεξικά, λεξικά με ένθετες λίστες, λεξικά με χαρακτήρες unicode, λεξικά με περίεργα κλειδιά και πολλά άλλα. Είναι πολύ πιο ενδελεχής από το να γράψετε μερικά χειροκίνητα παραδείγματα.
Σενάριο 2: Δοκιμή ενός Αλγόριθμου Ταξινόμησης
Ας ξαναδούμε το παράδειγμα ταξινόμησής μας. Δείτε πώς θα δοκιμάζατε τις ιδιότητες που ορίσαμε νωρίτερα.
from collections import Counter def my_buggy_sort(numbers): # Let's introduce a subtle bug: it drops duplicates return sorted(list(set(numbers))) @given(st.lists(st.integers())) def test_sorting_properties(numbers): sorted_list = my_buggy_sort(numbers) # Property 1: The output is sorted for i in range(len(sorted_list) - 1): assert sorted_list[i] <= sorted_list[i+1] # Property 2: The elements are the same (this will find the bug) assert Counter(numbers) == Counter(sorted_list) # Property 3: The function is idempotent assert my_buggy_sort(sorted_list) == sorted_list
Όταν εκτελέσετε αυτή τη δοκιμή, το Hypothesis θα βρει γρήγορα ένα αποτυχημένο παράδειγμα για την Ιδιότητα 2, όπως numbers=[0, 0]
. Η συνάρτησή μας επιστρέφει [0]
, και το Counter([0, 0])
δεν είναι ίσο με το Counter([0])
. Ο συρρικνωτής θα διασφαλίσει ότι το αποτυχημένο παράδειγμα είναι όσο το δυνατόν απλούστερο, καθιστώντας την αιτία του σφάλματος άμεσα προφανή.
Σενάριο 3: Stateful Testing
Για αντικείμενα με εσωτερική κατάσταση που αλλάζει με την πάροδο του χρόνου (όπως μια σύνδεση βάσης δεδομένων, ένα καλάθι αγορών ή μια κρυφή μνήμη), η εύρεση σφαλμάτων μπορεί να είναι απίστευτα δύσκολη. Μια συγκεκριμένη ακολουθία λειτουργιών μπορεί να απαιτείται για την πρόκληση βλάβης. Το Hypothesis παρέχει το RuleBasedStateMachine
ακριβώς για αυτόν τον σκοπό.
Φανταστείτε ένα απλό API για ένα in-memory key-value store:
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)
Μπορούμε να μοντελοποιήσουμε τη συμπεριφορά του και να το δοκιμάσουμε με μια μηχανή καταστάσεων:
from hypothesis.stateful import RuleBasedStateMachine, rule, Bundle class KeyValueStoreMachine(RuleBasedStateMachine): def __init__(self): super().__init__() self.model = {} self.sut = SimpleKeyValueStore() # Bundle() is used to pass data between rules 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() # To run the test, you simply subclass from the machine and unittest.TestCase # In pytest, you can simply assign the test to the machine class TestKeyValueStore = KeyValueStoreMachine.TestCase
Το Hypothesis θα εκτελέσει τώρα τυχαίες ακολουθίες `set_key`, `delete_key`, `get_key` και `check_size` λειτουργιών, προσπαθώντας αδιάκοπα να βρει μια ακολουθία που να προκαλέσει την αποτυχία μιας από τις δηλώσεις. Θα ελέγξει αν η ανάκτηση ενός διαγραμμένου κλειδιού λειτουργεί σωστά, αν το μέγεθος είναι συνεπές μετά από πολλαπλές ρυθμίσεις και διαγραφές, και πολλά άλλα σενάρια που ίσως δεν σκεφτείτε να δοκιμάσετε χειροκίνητα.
Βέλτιστες Πρακτικές και Προηγμένες Συμβουλές
- Η Βάση Δεδομένων Παραδειγμάτων: Το Hypothesis είναι έξυπνο. Όταν βρίσκει ένα σφάλμα, αποθηκεύει το αποτυχημένο παράδειγμα σε έναν τοπικό κατάλογο (
.hypothesis/
). Την επόμενη φορά που θα εκτελέσετε τις δοκιμές σας, θα αναπαράγει πρώτα αυτό το αποτυχημένο παράδειγμα, δίνοντάς σας άμεση ανατροφοδότηση ότι το σφάλμα εξακολουθεί να υπάρχει. Μόλις το διορθώσετε, το παράδειγμα δεν αναπαράγεται πλέον. - Έλεγχος Εκτέλεσης Δοκιμών με
@settings
: Μπορείτε να ελέγξετε πολλές πτυχές της εκτέλεσης δοκιμών χρησιμοποιώντας τον decorator@settings
. Μπορείτε να αυξήσετε τον αριθμό των παραδειγμάτων, να ορίσετε μια προθεσμία για το πόσο μπορεί να εκτελεστεί ένα μόνο παράδειγμα (για να πιάσετε άπειρους βρόχους) και να απενεργοποιήσετε ορισμένους ελέγχους υγείας.@settings(max_examples=500, deadline=1000) # Run 500 examples, 1-second deadline @given(...) ...
- Αναπαραγωγή Αποτυχιών: Κάθε εκτέλεση του Hypothesis εκτυπώνει μια τιμή αρχικού σπόρου (seed value) (π.χ.,
@reproduce_failure('version', 'seed')
). Αν ένας CI server βρει ένα σφάλμα που δεν μπορείτε να αναπαράγετε τοπικά, μπορείτε να χρησιμοποιήσετε αυτόν τον decorator με τον παρεχόμενο σπόρο για να αναγκάσετε το Hypothesis να εκτελέσει την ακριβώς ίδια ακολουθία παραδειγμάτων. - Ενσωμάτωση με CI/CD: Το Hypothesis ταιριάζει απόλυτα σε κάθε pipeline συνεχούς ενσωμάτωσης. Η ικανότητά του να βρίσκει ασαφή σφάλματα πριν φτάσουν στην παραγωγή το καθιστά ένα ανεκτίμητο δίχτυ ασφαλείας.
Η Αλλαγή Νοοτροπίας: Σκέψη σε Ιδιότητες
Η υιοθέτηση του Hypothesis είναι κάτι περισσότερο από την εκμάθηση μιας νέας βιβλιοθήκης· πρόκειται για την υιοθέτηση ενός νέου τρόπου σκέψης σχετικά με την ορθότητα του κώδικά σας. Αντί να ρωτάτε, "Ποιες εισόδους πρέπει να δοκιμάσω;", αρχίζετε να ρωτάτε, "Ποιες είναι οι καθολικές αλήθειες σχετικά με αυτόν τον κώδικα;"
Ακολουθούν μερικές ερωτήσεις για να σας καθοδηγήσουν όταν προσπαθείτε να εντοπίσετε ιδιότητες:
- Υπάρχει αντίστροφη λειτουργία; (π.χ., σειριοποίηση/αποσειριοποίηση, κρυπτογράφηση/αποκρυπτογράφηση, συμπίεση/αποσυμπίεση). Η ιδιότητα είναι ότι η εκτέλεση της λειτουργίας και της αντίστροφής της θα πρέπει να αποδίδει την αρχική είσοδο.
- Είναι η λειτουργία ιδεοδύναμη; (π.χ.,
abs(abs(x)) == abs(x)
). Η εφαρμογή της συνάρτησης περισσότερες από μία φορές θα πρέπει να παράγει το ίδιο αποτέλεσμα με την εφαρμογή της μία φορά. - Υπάρχει ένας διαφορετικός, απλούστερος τρόπος για να υπολογίσετε το ίδιο αποτέλεσμα; Μπορείτε να δοκιμάσετε ότι η πολύπλοκη, βελτιστοποιημένη συνάρτησή σας παράγει την ίδια έξοδο με μια απλή, προφανώς σωστή έκδοση (π.χ., δοκιμάζοντας τη φανταχτερή σας ταξινόμηση έναντι της ενσωματωμένης
sorted()
της Python). - Τι πρέπει να ισχύει πάντα για την έξοδο; (π.χ., η έξοδος μιας `find_prime_factors` συνάρτησης θα πρέπει να περιέχει μόνο πρώτους αριθμούς, και το γινόμενό τους θα πρέπει να είναι ίσο με την είσοδο).
- Πώς αλλάζει η κατάσταση; (Για stateful testing) Ποια αμετάβλητα πρέπει να διατηρούνται μετά από οποιαδήποτε έγκυρη λειτουργία; (π.χ., Ο αριθμός των ειδών σε ένα καλάθι αγορών δεν μπορεί ποτέ να είναι αρνητικός).
Συμπέρασμα: Ένα Νέο Επίπεδο Εμπιστοσύνης
Το property-based testing με το Hypothesis δεν αντικαθιστά το testing βάσει παραδειγμάτων. Χρειάζεστε ακόμα συγκεκριμένες, χειρόγραφες δοκιμές για κρίσιμη επιχειρηματική λογική και καλά κατανοητές απαιτήσεις (π.χ., "Ένας χρήστης από τη χώρα Χ πρέπει να δει την τιμή Υ").
Αυτό που παρέχει το Hypothesis είναι ένας ισχυρός, αυτοματοποιημένος τρόπος για να εξερευνήσετε τη συμπεριφορά του κώδικά σας και να προφυλαχθείτε από απρόβλεπτες ακραίες περιπτώσεις. Λειτουργεί ως ένας ακούραστος συνεργάτης, δημιουργώντας χιλιάδες δοκιμές που είναι πιο ποικίλες και πανούργες από ό,τι θα μπορούσε ρεαλιστικά να γράψει οποιοσδήποτε άνθρωπος. Ορίζοντας τις θεμελιώδεις ιδιότητες του κώδικά σας, δημιουργείτε μια ισχυρή προδιαγραφή έναντι της οποίας μπορεί να δοκιμάσει το Hypothesis, δίνοντάς σας ένα νέο επίπεδο εμπιστοσύνης στο λογισμικό σας.
Την επόμενη φορά που θα γράψετε μια συνάρτηση, αφιερώστε λίγο χρόνο να σκεφτείτε πέρα από τα παραδείγματα. Ρωτήστε τον εαυτό σας, "Ποιοι είναι οι κανόνες; Τι πρέπει να ισχύει πάντα;" Στη συνέχεια, αφήστε το Hypothesis να κάνει τη δύσκολη δουλειά προσπαθώντας να τους παραβιάσει. Θα εκπλαγείτε με αυτά που θα βρει, και ο κώδικάς σας θα γίνει καλύτερος.