Skip to content

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

Python
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

  1. Generators: Strategies that produce random values (st.integers(), st.text(), custom strategies)
  2. Properties: Invariants that should hold for all generated inputs
  3. 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

Python
@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:

Python
@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

Python
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

Python
@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:

Python
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:

  1. Stateful testing is hard: You need RuleBasedStateMachine, which is verbose
  2. No automatic rollback: Can't branch from intermediate states
  3. Sequence exploration isn't the focus: Hypothesis optimizes for input diversity, not path coverage

Order-Dependent Bugs

Python
# 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:

Python
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

Python
# 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

Python
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:

Text Only
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   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:

Python
# 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

Python
# 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

Python
# 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

YAML
# .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


Keywords: property-based testing, Hypothesis Python, QuickCheck, generative testing, fuzzing, stateful testing, API testing, sequence exploration