VenomQA vs Property-Based Testing: A Practical Comparison¶
Property-based testing revolutionized how we find bugs by automatically generating thousands of test cases. Tools like QuickCheck, Hypothesis, and similar frameworks have become essential for discovering edge cases that humans miss.
But here's the thing: property-based testing excels at fuzzing inputs, while VenomQA excels at exploring sequences. They solve different problems, and understanding when to use each can dramatically improve your testing strategy.
1. What is Property-Based Testing?¶
The QuickCheck Revolution¶
Property-based testing originated with QuickCheck (Claesson & Hughes, 2000), a Haskell tool that turned testing on its head. Instead of writing individual test cases, you describe properties that should always hold, and the framework generates random inputs to try to falsify them.
How It Works¶
from hypothesis import given, strategies as st
@given(st.integers(), st.integers())
def test_addition_commutative(x, y):
assert x + y == y + x
The framework generates hundreds of random (x, y) pairs:
- Positive integers
- Negative integers
- Zero
- Very large numbers
- Edge cases at type boundaries
Core Concepts¶
- Generators: Strategies that produce random values (
st.integers(),st.text(), custom strategies) - Properties: Invariants that should hold for all generated inputs
- Shrinking: When a bug is found, automatically reduce to the minimal failing case
Hypothesis (Python), QuickCheck (Haskell), ScalaCheck, fast-check (JavaScript) β they all follow this pattern.
2. What Property-Based Testing Does Well¶
Property-based testing shines when you need to explore the input space of individual functions or endpoints.
Input Fuzzing¶
@given(st.text())
def test_parse_never_crashes(input_string):
result = parse_json(input_string)
assert result is not None or input_string is not None
Hypothesis will try: - Empty strings - Unicode characters - Very long strings (megabytes) - Malformed JSON - Control characters - Null bytes
Edge Cases in Parsing¶
Property-based testing finds bugs at type boundaries:
@given(st.integers(min_value=0, max_value=2**31-1))
def test_user_age_reasonable(age):
user = create_user(age=age)
assert user.age >= 0
assert user.age < 150 # Wait, what about overflow?
Schema Validation¶
from hypothesis import given, strategies as st
user_strategy = st.fixed_dictionaries({
'name': st.text(min_size=1, max_size=100),
'email': st.from_regex(r'[a-z]+@[a-z]+\.[a-z]+'),
'age': st.integers(min_value=0, max_value=120),
})
@given(user_strategy)
def test_user_schema_valid(user_data):
validated = UserSchema(**user_data)
assert validated.name == user_data['name']
Type Boundary Conditions¶
@given(st.integers())
def test_int_serialization_roundtrip(n):
serialized = json.dumps(n)
deserialized = json.loads(serialized)
assert deserialized == n # Catches integer overflow in JSON
Summary: Property-based testing is a fuzzer for inputs. It asks: "Given random inputs, does my function behave correctly?"
3. What Property-Based Testing Misses¶
Here's the gap: real bugs often hide in sequences of operations, not in individual function calls.
The Sequence Problem¶
Consider a payment API:
order = create_order(amount=100) # β Works
refund_order(order.id, amount=100) # β Works
refund_order(order.id, amount=100) # π₯ Double refund!
Property-based testing with Hypothesis can't easily find this because:
- Stateful testing is hard: You need
RuleBasedStateMachine, which is verbose - No automatic rollback: Can't branch from intermediate states
- Sequence exploration isn't the focus: Hypothesis optimizes for input diversity, not path coverage
Order-Dependent Bugs¶
# These work in isolation
update_user(user_id, role="admin") # β
delete_user(user_id) # β
# But this sequence exposes a bug
delete_user(user_id)
update_user(user_id, role="admin") # π₯ Updates deleted user!
Hypothesis can test stateful systems with RuleBasedStateMachine, but it's:
class PaymentMachine(RuleBasedStateMachine):
def __init__(self):
super().__init__()
self.orders = {}
@rule(amount=st.integers(min_value=1, max_value=10000))
def create(self, amount):
order_id = create_order(amount)
self.orders[order_id] = amount
@rule(order_id=st.sampled_from(...))
def refund(self, order_id):
# But how do we test refund -> refund?
# How do we branch and explore ALL sequences?
pass
It works, but it's not designed for exhaustive sequence exploration.
Multi-Step Workflows¶
# E-commerce checkout flow
add_to_cart(item_id) # State: cart has item
apply_coupon("SAVE20") # State: discount applied
checkout() # State: order created
ship_order() # State: order shipped
deliver_order() # State: order delivered
cancel_order() # π₯ Can't cancel delivered order!
Testing cancel at each state requires:
- 5 different starting states
- Each state has different valid next actions
- Combinatorial explosion of paths
Property-based testing doesn't provide tools for systematic state graph exploration.
4. VenomQA's Approach¶
VenomQA was designed specifically for sequence exploration with automatic state management.
Focused on Sequences, Not Inputs¶
from venomqa import Action, Agent, BFS, Invariant, World
from venomqa.adapters.http import HttpClient
def create_order(api, context):
resp = api.post("/orders", json={"amount": 100})
context.set("order_id", resp.json()["id"])
return resp
def refund_order(api, context):
order_id = context.get("order_id")
if order_id is None:
return None # Skip - precondition not met
return api.post(f"/orders/{order_id}/refund")
api = HttpClient(base_url="http://localhost:8000")
world = World(api=api, state_from_context=["order_id"])
agent = Agent(
world=world,
actions=[
Action(name="create_order", execute=create_order),
Action(name="refund_order", execute=refund_order),
],
invariants=[...],
strategy=BFS(),
max_steps=50,
)
agent.explore()
VenomQA will explore:
- create_order
- create_order β refund_order
- create_order β refund_order β refund_order (finds double refund bug!)
- And more paths...
State Graph Exploration¶
VenomQA treats your API as a directed graph:
βββββββββββββββ
β START β
ββββββββ¬βββββββ
β create_order
βΌ
βββββββββββββββ
β order_id=123β
ββββββββ¬βββββββ
β β
refund β β refund
βΌ βΌ
ββββββββββββ ββββββββββββββββ
β refunded β β double refundβ β BUG!
ββββββββββββ ββββββββββββββββ
Database Rollback for Branching¶
The key innovation: database savepoints let VenomQA branch and reset:
# Explore path: create β refund β refund
checkpoint() # Save state
refund_order() # First refund
refund_order() # Second refund - find bug!
rollback() # Reset to before refunds
# Now explore: create β cancel
cancel_order() # Different path from same state
Without rollback, you'd need to: 1. Start fresh server 2. Recreate all state 3. Run one path 4. Repeat for each path
With rollback: one test run, thousands of paths explored.
5. Comparison Table¶
| Aspect | Hypothesis | VenomQA |
|---|---|---|
| Focus | Input fuzzing | Sequence exploration |
| State | Stateless (or manual stateful) | Stateful with auto-rollback |
| Rollback | No built-in rollback | Database savepoint/restore |
| Test Style | Generate random inputs | Explore action sequences |
| Bug Type | Edge cases in parsing/validation | Workflow logic bugs |
| Best For | Libraries, parsers, pure functions | APIs, state machines, workflows |
| Learning Curve | Low (decorators) | Medium (actions/invariants) |
| Stateful Testing | Via RuleBasedStateMachine |
Native design |
| Shrinking | Yes (minimal failing input) | Yes (minimal failing sequence) |
| Integration | Pytest plugin | Standalone + reporters |
6. Using Both Together¶
The best testing strategy uses both tools for their strengths.
Hypothesis for Input Validation¶
# tests/test_schemas.py
from hypothesis import given, strategies as st
@given(st.integers(min_value=1, max_value=1000000))
def test_order_amount_valid(amount):
"""Ensure amount validation handles all edge cases."""
order = create_order_sync(amount)
assert order.amount == amount
@given(st.text())
def test_product_name_never_crashes(name):
"""Fuzz product names for injection/crashes."""
product = create_product(name)
assert product is not None
VenomQA for Workflow Testing¶
# tests/test_workflows.py
from venomqa import Action, Agent, BFS, Invariant, World
# Define workflow actions
actions = [
Action(name="create_order", execute=create_order),
Action(name="refund_order", execute=refund_order),
Action(name="cancel_order", execute=cancel_order),
Action(name="ship_order", execute=ship_order),
]
# Define invariants
invariants = [
Invariant(
name="no_500_errors",
check=lambda world: world.context.get("last_status", 200) < 500,
),
Invariant(
name="order_consistency",
check=check_order_state_consistent,
),
]
# Run exploration
agent = Agent(
world=World(api=api, state_from_context=["order_id"]),
actions=actions,
invariants=invariants,
strategy=BFS(),
max_steps=100,
)
result = agent.explore()
Example CI Setup¶
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
unit-and-property:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests with Hypothesis
run: pytest tests/ --hypothesis-profile=ci
workflow-exploration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
api:
image: myapp:test
steps:
- uses: actions/checkout@v4
- name: Run VenomQA exploration
run: venomqa run --config venomqa.yaml --max-steps 500
Run Hypothesis on every commit (fast, catches input bugs).
Run VenomQA nightly or on PRs (slower, catches workflow bugs).
7. When to Choose Which¶
Use Hypothesis When:¶
- β Testing pure functions (no state)
- β Fuzzing input parsers (JSON, CSV, custom formats)
- β Validating schemas with many edge cases
- β Testing type boundaries (int overflow, string lengths)
- β You need fast feedback in unit tests
- β Your code is mostly stateless logic
Example: A JSON parser, a date formatting library, a calculator API.
Use VenomQA When:¶
- β Testing stateful APIs (CRUD, e-commerce, payments)
- β Finding bugs in workflows (checkout flows, approval chains)
- β Testing order-dependent behavior
- β Exploring all possible paths through a system
- β You have a database that supports savepoints
- β Testing multi-step user journeys
Example: A payment processor, an e-commerce platform, a booking system.
Use Both When:¶
- β You have a stateful API with complex input validation
- β You want comprehensive coverage of both inputs and workflows
- β You're building a critical system where bugs are expensive
- β You have time for thorough testing in CI
Example: A fintech API, a healthcare system, an e-commerce platform.
Summary¶
| Testing Need | Tool |
|---|---|
| "Does this function handle weird inputs?" | Hypothesis |
| "Does this workflow handle all sequences?" | VenomQA |
| "Both!" | Use both |
Property-based testing and stateful exploration are complementary, not competing. Hypothesis finds bugs in what data you accept. VenomQA finds bugs in what sequences of operations you support.
Use them together for a testing strategy that covers both the input space and the state space.
Further Reading¶
- Hypothesis Documentation
- QuickCheck: A Lightweight Tool for Random Testing
- VenomQA: Stateful API Testing
- Testing Stateful Systems with Hypothesis
Keywords: property-based testing, Hypothesis Python, QuickCheck, generative testing, fuzzing, stateful testing, API testing, sequence exploration