pytest-bdd-property 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 pytest-bdd-property Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,422 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-bdd-property
3
+ Version: 0.1.0
4
+ Summary: Property-based testing plugin for pytest-bdd — express universal invariants in standard Gherkin, executed by Hypothesis
5
+ Author: pytest-bdd-property Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/bryonjacob/pytest-bdd-property
8
+ Project-URL: Repository, https://github.com/bryonjacob/pytest-bdd-property
9
+ Project-URL: Issues, https://github.com/bryonjacob/pytest-bdd-property/issues
10
+ Keywords: pytest,bdd,property-based-testing,gherkin,hypothesis,pbt
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: pytest>=8.0
24
+ Requires-Dist: pytest-bdd>=7.0
25
+ Requires-Dist: hypothesis>=6.0
26
+ Dynamic: license-file
27
+
28
+ # pytest-bdd-property
29
+
30
+ Property-based testing for Gherkin scenarios, powered by [Hypothesis](https://hypothesis.readthedocs.io/).
31
+
32
+ Express universal invariants in standard Gherkin, and have Hypothesis generate 100+ inputs to find counterexamples automatically.
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Tag your scenario with `@property-based`
37
+
38
+ ```gherkin
39
+ @property-based
40
+ Feature: Password Hashing Properties
41
+
42
+ Scenario: A password always verifies against its own hash
43
+ Given any valid password <P>
44
+ When <P> is hashed producing <H>
45
+ Then <P> verifies against <H>
46
+ ```
47
+
48
+ ### 2. Register your conftest step definitions
49
+
50
+ In your root `conftest.py`:
51
+
52
+ ```python
53
+ import pytest_bdd_property.plugin # registers pytest hooks
54
+
55
+ from pytest_bdd import given, when, then, parsers
56
+ from pytest_bdd_property.plugin import (
57
+ handle_given_step,
58
+ handle_when_step,
59
+ handle_then_step,
60
+ )
61
+
62
+ @given(parsers.re(r"(?P<text>any .+|<.+)"))
63
+ def property_given_step(text):
64
+ handle_given_step(text)
65
+
66
+ @when(parsers.re(r"(?P<text><.+)"))
67
+ def property_when_step(text):
68
+ handle_when_step(text)
69
+
70
+ @then(parsers.re(r"(?P<text><.+)"))
71
+ def property_then_step(text):
72
+ handle_then_step(text)
73
+ ```
74
+
75
+ ### 3. Write property step definitions
76
+
77
+ ```python
78
+ from pytest_bdd import scenarios
79
+ from pytest_bdd_property import register_strategy, property_when, property_then
80
+ from hypothesis import strategies as st
81
+
82
+ scenarios("my_feature.feature")
83
+
84
+ register_strategy("valid password", lambda: st.text(min_size=8, max_size=128))
85
+
86
+ @property_when(r"<(\w+)> is hashed producing <(\w+)>")
87
+ def hash_step(vals, results, pw_var, hash_var):
88
+ results[hash_var] = hash_password(vals[pw_var])
89
+
90
+ @property_then(r"<(\w+)> verifies against <(\w+)>")
91
+ def verify_step(vals, results, pw_var, hash_var):
92
+ assert verify_password(vals[pw_var], results[hash_var])
93
+ ```
94
+
95
+ ### 4. Run tests
96
+
97
+ ```bash
98
+ pytest tests/ -v
99
+ ```
100
+
101
+ Hypothesis runs each `@property-based` scenario 100 times with generated inputs. On failure, it **shrinks** to find the minimal counterexample.
102
+
103
+ ---
104
+
105
+ ## How It Works
106
+
107
+ ### Two-Phase Execution Model
108
+
109
+ **Phase 1 (Registration):** During normal pytest-bdd step execution:
110
+ - `Given any <type> <var>` → registers a Hypothesis strategy
111
+ - `And <A> is not equal to <B>` → registers an assumption (filter)
112
+ - `When`/`Then` steps → register callbacks (don't execute yet)
113
+
114
+ **Phase 2 (Execution):** After all steps run, a pytest hook triggers:
115
+ - Hypothesis builds a composite strategy from all registered strategies
116
+ - Runs the property 100+ times with generated inputs
117
+ - Applies assumptions via `hypothesis.assume()`
118
+ - Executes When callbacks (actions), then Then callbacks (assertions)
119
+ - On failure: shrinks to find the minimal counterexample
120
+
121
+ ### Architecture
122
+
123
+ ```
124
+ conftest.py test_my_feature.py
125
+ ├── plugin hooks ├── scenarios("my.feature")
126
+ └── catch-all steps ├── register_strategy(...)
127
+ │ ├── @property_when(...)
128
+ ▼ └── @property_then(...)
129
+ plugin.py
130
+ ├── pytest_runtest_setup → detect @property-based tag
131
+ ├── handle_given_step → strategy + assumption registration
132
+ ├── handle_when_step → action callback registration
133
+ ├── handle_then_step → assertion callback registration
134
+ └── pytest_runtest_call → run_property_test() via Hypothesis
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Built-in Strategies
140
+
141
+ Use these directly in your feature files with `Given any <type> <var>`:
142
+
143
+ ### Primitives
144
+
145
+ | Strategy Name | Description | Hypothesis Equivalent |
146
+ |---------------|-------------|-----------------------|
147
+ | `text` | Arbitrary Unicode strings | `st.text()` |
148
+ | `non-empty text` | Strings with length ≥ 1 | `st.text(min_size=1)` |
149
+ | `integer` | Arbitrary integers | `st.integers()` |
150
+ | `positive integer` | Integers ≥ 1 | `st.integers(min_value=1)` |
151
+ | `negative integer` | Integers ≤ -1 | `st.integers(max_value=-1)` |
152
+ | `natural` | Integers ≥ 0 | `st.integers(min_value=0)` |
153
+ | `float` | Floats (no NaN/Infinity) | `st.floats(allow_nan=False, allow_infinity=False)` |
154
+ | `boolean` | True or False | `st.booleans()` |
155
+
156
+ ### Strings
157
+
158
+ | Strategy Name | Description | Hypothesis Equivalent |
159
+ |---------------|-------------|-----------------------|
160
+ | `ascii text` | ASCII-only strings | `st.text(alphabet=st.characters(codec="ascii"))` |
161
+ | `alphanumeric` | `[a-z0-9]+` strings | `st.from_regex(r"[a-z0-9]+")` |
162
+ | `hex string` | `[0-9a-f]+` strings | `st.from_regex(r"[0-9a-f]+")` |
163
+
164
+ ### Identifiers
165
+
166
+ | Strategy Name | Description | Hypothesis Equivalent |
167
+ |---------------|-------------|-----------------------|
168
+ | `uuid` | UUID v4 strings | `st.uuids().map(str)` |
169
+ | `email` | Email-shaped strings | `st.emails()` |
170
+ | `url` | URL-shaped strings | `st.from_regex(...)` |
171
+
172
+ ### Temporal
173
+
174
+ | Strategy Name | Description | Hypothesis Equivalent |
175
+ |---------------|-------------|-----------------------|
176
+ | `date` | Date objects | `st.dates()` |
177
+
178
+ ### Structured
179
+
180
+ | Strategy Name | Description | Hypothesis Equivalent |
181
+ |---------------|-------------|-----------------------|
182
+ | `json value` | Recursive JSON values | `st.recursive(...)` |
183
+ | `json object` | `dict[str, str]` | `st.dictionaries(st.text(), st.text())` |
184
+
185
+ ### Domain Defaults
186
+
187
+ | Strategy Name | Description | Hypothesis Equivalent |
188
+ |---------------|-------------|-----------------------|
189
+ | `password` | Text, 8-128 chars | `st.text(min_size=8, max_size=128)` |
190
+ | `username` | `[a-z0-9_]{3,32}` | `st.from_regex(...)` |
191
+
192
+ ### Custom Strategies
193
+
194
+ Register domain-specific strategies in your test file:
195
+
196
+ ```python
197
+ from pytest_bdd_property import register_strategy
198
+ from hypothesis import strategies as st
199
+
200
+ register_strategy("valid password", lambda: (
201
+ st.tuples(
202
+ st.text(alphabet="abcdefghijklmnopqrstuvwxyz", min_size=2, max_size=10),
203
+ st.text(alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ", min_size=2, max_size=10),
204
+ st.text(alphabet="0123456789", min_size=2, max_size=5),
205
+ st.text(alphabet="!@#$%^&*", min_size=2, max_size=3),
206
+ ).map(lambda parts: parts[0] + parts[1] + parts[2] + parts[3])
207
+ ))
208
+ ```
209
+
210
+ Then in Gherkin:
211
+ ```gherkin
212
+ Given any valid password <P>
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Built-in Assumptions
218
+
219
+ Use these in `Given`/`And` steps to filter generated inputs:
220
+
221
+ ### Equality
222
+
223
+ | Pattern | Meaning | Maps To |
224
+ |---------|---------|---------|
225
+ | `<A> is not equal to <B>` | A ≠ B | `assume(A != B)` |
226
+ | `<A> is equal to <B>` | A = B | `assume(A == B)` |
227
+
228
+ ### Numeric Comparison
229
+
230
+ | Pattern | Meaning | Maps To |
231
+ |---------|---------|---------|
232
+ | `<A> is greater than <B>` | A > B | `assume(A > B)` |
233
+ | `<A> is less than <B>` | A < B | `assume(A < B)` |
234
+ | `<A> is greater than or equal to <B>` | A ≥ B | `assume(A >= B)` |
235
+ | `<A> is less than or equal to <B>` | A ≤ B | `assume(A <= B)` |
236
+
237
+ ### Emptiness
238
+
239
+ | Pattern | Meaning | Maps To |
240
+ |---------|---------|---------|
241
+ | `<A> is not empty` | len(A) > 0 | `assume(len(A) > 0)` |
242
+ | `<A> is empty` | len(A) = 0 | `assume(len(A) == 0)` |
243
+
244
+ ### Length
245
+
246
+ | Pattern | Meaning | Maps To |
247
+ |---------|---------|---------|
248
+ | `<A> has length greater than N` | len(A) > N | `assume(len(A) > N)` |
249
+ | `<A> has length less than N` | len(A) < N | `assume(len(A) < N)` |
250
+
251
+ ### Containment
252
+
253
+ | Pattern | Meaning | Maps To |
254
+ |---------|---------|---------|
255
+ | `<A> contains <B>` | B in A | `assume(B in A)` |
256
+ | `<A> does not contain <B>` | B not in A | `assume(B not in A)` |
257
+
258
+ ### Type Checks
259
+
260
+ | Pattern | Meaning | Maps To |
261
+ |---------|---------|---------|
262
+ | `<A> is a number` | isinstance(A, (int, float)) | `assume(isinstance(A, (int, float)))` |
263
+ | `<A> is a string` | isinstance(A, str) | `assume(isinstance(A, str))` |
264
+
265
+ ### Custom Assumptions
266
+
267
+ ```python
268
+ from pytest_bdd_property import register_assumption
269
+
270
+ register_assumption(
271
+ r"^<(\w+)> is a valid email$",
272
+ lambda var: lambda vals: "@" in str(vals[var])
273
+ )
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Configuration via Tags
279
+
280
+ ```gherkin
281
+ @property-based @num-runs:500 @seed:42 @verbose
282
+ Scenario: Stress test hashing
283
+ ...
284
+ ```
285
+
286
+ | Tag | Effect |
287
+ |-----|--------|
288
+ | `@num-runs:<n>` | Override number of generated examples (default: 100) |
289
+ | `@seed:<n>` | Fix the random seed for reproducibility |
290
+ | `@verbose` | Enable verbose output |
291
+
292
+ ---
293
+
294
+ ## Common Patterns
295
+
296
+ ### Round-trip (Serialization)
297
+
298
+ ```gherkin
299
+ @property-based
300
+ Scenario: JSON round-trip preserves data
301
+ Given any json object <D>
302
+ When <D> is serialized to JSON producing <J>
303
+ And <J> is deserialized producing <D2>
304
+ Then <D> is equal to <D2>
305
+ ```
306
+
307
+ ### Idempotency
308
+
309
+ ```gherkin
310
+ @property-based
311
+ Scenario: Normalizing email twice gives the same result
312
+ Given any email <E>
313
+ When <E> is normalized producing <N1>
314
+ And <N1> is normalized producing <N2>
315
+ Then <N1> is equal to <N2>
316
+ ```
317
+
318
+ ### No False Positives
319
+
320
+ ```gherkin
321
+ @property-based
322
+ Scenario: Wrong password never verifies
323
+ Given any valid password <P>
324
+ And any valid password <Q>
325
+ And <P> is not equal to <Q>
326
+ When <P> is hashed producing <H>
327
+ Then <Q> does not verify against <H>
328
+ ```
329
+
330
+ ### No Information Leakage
331
+
332
+ ```gherkin
333
+ @property-based
334
+ Scenario: Hash never contains plaintext
335
+ Given any valid password <P>
336
+ When <P> is hashed producing <H>
337
+ Then <H> does not contain <P>
338
+ ```
339
+
340
+ ---
341
+
342
+ ## API Reference
343
+
344
+ ### `register_strategy(name, factory)`
345
+
346
+ Register a named strategy for `Given any <name> <var>`.
347
+
348
+ - `name`: Case-insensitive strategy name
349
+ - `factory`: `() -> SearchStrategy` callable
350
+
351
+ ### `property_when(pattern)`
352
+
353
+ Decorator to register a When step for property-based scenarios.
354
+
355
+ ```python
356
+ @property_when(r"<(\w+)> is hashed producing <(\w+)>")
357
+ def hash_step(vals, results, pw_var, hash_var):
358
+ results[hash_var] = hash_password(vals[pw_var])
359
+ ```
360
+
361
+ - `vals`: `dict[str, Any]` — generated values keyed by variable name
362
+ - `results`: `dict[str, Any]` — intermediate results from When steps
363
+ - Additional args: regex capture groups from the pattern
364
+
365
+ ### `property_then(pattern)`
366
+
367
+ Decorator to register a Then step for property-based scenarios.
368
+
369
+ ```python
370
+ @property_then(r"<(\w+)> verifies against <(\w+)>")
371
+ def verify_step(vals, results, pw_var, hash_var):
372
+ assert verify_password(vals[pw_var], results[hash_var])
373
+ ```
374
+
375
+ Raise `AssertionError` to signal a property violation. Hypothesis shrinks to minimal counterexample.
376
+
377
+ ### `register_assumption(pattern, builder)`
378
+
379
+ Register a custom assumption pattern.
380
+
381
+ ```python
382
+ register_assumption(
383
+ r"^<(\w+)> is a valid email$",
384
+ lambda var: lambda vals: "@" in str(vals[var])
385
+ )
386
+ ```
387
+
388
+ ### `resolve_strategy(name)`
389
+
390
+ Resolve a strategy name to a Hypothesis `SearchStrategy`. Raises `ValueError` if not found.
391
+
392
+ ### `list_strategies()`
393
+
394
+ Return all registered strategy names, sorted.
395
+
396
+ ---
397
+
398
+ ## Coexistence with Behavioral Scenarios
399
+
400
+ Property-based and behavioral scenarios coexist naturally:
401
+
402
+ ```gherkin
403
+ Feature: User Authentication
404
+
405
+ # Behavioral (concrete examples)
406
+ Scenario: User can log in with correct password
407
+ Given a user with password "hunter2"
408
+ When they log in with "hunter2"
409
+ Then they are authenticated
410
+
411
+ # Property (universal invariant)
412
+ @property-based
413
+ Scenario: Wrong password never authenticates
414
+ Given any text <P>
415
+ And any text <Q>
416
+ And <P> is not equal to <Q>
417
+ Given a user with password <P>
418
+ When they log in with <Q>
419
+ Then they are rejected
420
+ ```
421
+
422
+ The `@property-based` tag tells the plugin to intercept execution. Scenarios without the tag run normally through pytest-bdd.