techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 7)
cc036ff verified
|
Raw
History Blame Contribute Delete
16.2 kB
# Property-Based Test: [Invariant Name]
## Purpose
Validate invariant: [invariant statement] for [target module/function]
**What this test validates:**
- Invariant holds for all valid inputs (not just hand-picked examples)
- Edge cases discovered through automatic test generation
- Counterexample shrinking to minimal failing case
**Target:**
- Module: `backend/core/[module].py`
- Function: `[function_name]`
- Input type: `[strings, integers, lists, JSON, etc.]`
## Dependencies
**Required Libraries:**
```bash
pip install hypothesis==6.92.0
```
**Target Module:**
- `backend/core/[module].py` - [description of target]
- `backend/api/[routes].py` - [description of target API]
**Hypothesis Strategies:**
- `hypothesis.strategies.text()` - String generation
- `hypothesis.strategies.integers()` - Integer generation
- `hypothesis.strategies.lists()` - List generation
- `hypothesis.strategies.dictionaries()` - Dictionary generation
- `hypothesis.strategies.builds()` - Custom object generation
## Invariant (Document Before Writing Test)
**CRITICAL: Document invariant FIRST, then write test.**
**Property:** [What must be true for all inputs]
Example invariants:
- "Workflow serialization is lossless for all step lists"
- "Agent execution is idempotent for all agent IDs"
- "JSON round-trip preserves data for all valid JSON objects"
- "Episode segmentation produces contiguous time segments for all message lists"
**Domain:** [Input space: strings, integers, lists, JSON objects, etc.]
**Preconditions:** [Required conditions for invariant to hold]
Example:
- Input must be valid UTF-8 string
- List must contain 0-100 items
- JSON must conform to schema
**Postconditions:** [What must be true after operation]
Example:
- Output list has same length as input
- All IDs in output are valid UUIDs
- Timestamps are monotonically increasing
**Example Invariant Documentation:**
```python
"""
Invariant: Workflow serialization is lossless for all step lists.
Property:
- For any list of workflow steps, serializing and deserializing
produces an equivalent workflow with the same steps.
Domain:
- Input: List of workflow steps (dict with 'action', 'params', 'order')
- Size: 0-100 steps per workflow
- Actions: 'create_agent', 'execute_workflow', 'present_canvas'
Preconditions:
- All steps have valid 'action' field
- All steps have 'order' field (integer, 0-1000)
- 'params' is a dict (can be empty)
Postconditions:
- Deserialized workflow has same number of steps as input
- All steps are present in same order
- All step fields are preserved (action, params, order)
"""
```
## Setup
**Hypothesis settings configuration:**
```python
from hypothesis import given, settings, strategies as st
from tests.property_tests.conftest import DEFAULT_PROFILE, CI_PROFILE
# Use CI profile for faster tests (50 examples, 5s deadline)
# Use local profile for thorough testing (200 examples, 30s deadline)
# Settings profiles defined in tests/property_tests/conftest.py:
# - CI_PROFILE: max_examples=50, deadline=5s (fast for PR checks)
# - DEFAULT_PROFILE: max_examples=200, deadline=30s (thorough for local)
# Example: Use CI profile for fast tests
@settings(CI_PROFILE)
# Example: Use default profile for thorough tests
@settings(DEFAULT_PROFILE)
# Custom settings
@settings(
max_examples=100,
deadline=timedelta(seconds=10),
phases=[Phase.generate] # Skip reuse phase for faster tests
)
```
**Import strategies:**
```python
from hypothesis import strategies as st
# Common strategies
st.text() # Random strings (unicode, min_size=0)
st.integers(min_value=0, max_value=100) # Bounded integers
st.lists(st.integers(), min_size=0, max_size=100) # Lists
st.dictionaries(st.text(), st.integers()) # Dicts
st.builds(MyModel, id=st.uuid4(), name=st.text()) # Custom objects
```
## Test Procedure
**Step 1: Define invariant (BEFORE writing test)**
```python
# Document invariant in docstring
def test_workflow_serialization_roundtrip(steps):
"""
Test that workflow serialization is lossless.
Invariant: Serializing and deserializing a workflow produces
an equivalent workflow with the same steps.
Strategy:
- Generate random workflow steps (0-100 steps)
- Each step has: action, params, order
- Actions from: create_agent, execute_workflow, present_canvas
Expected:
- Deserialized workflow has same steps as input
- All fields preserved (action, params, order)
- Steps in same order
"""
```
**Step 2: Write test with @given decorator**
```python
import pytest
from hypothesis import given, settings, strategies as st
from tests.property_tests.conftest import DEFAULT_PROFILE
from backend.core.workflow_engine import WorkflowDefinition
@pytest.mark.property
@given(st.lists(
st.fixed_dictionaries({
'action': st.sampled_from(['create_agent', 'execute_workflow', 'present_canvas']),
'params': st.dictionaries(st.text(), st.text()),
'order': st.integers(min_value=0, max_value=1000)
}),
min_size=0,
max_size=100
))
@settings(DEFAULT_PROFILE)
def test_workflow_serialization_roundtrip(steps):
"""
Test that workflow serialization is lossless for all step lists.
Invariant: For any list of workflow steps, serializing and deserializing
produces an equivalent workflow with the same steps.
Strategy: Generate 0-100 workflow steps with random actions, params, order.
Expected: All steps preserved in same order.
"""
# Arrange: Create workflow from generated steps
workflow = WorkflowDefinition(name="test", steps=steps)
# Act: Serialize and deserialize
serialized = workflow.serialize()
deserialized = WorkflowDefinition.deserialize(serialized)
# Assert: Invariant holds
assert len(deserialized.steps) == len(steps), \
f"Step count mismatch: {len(deserialized.steps)} != {len(steps)}"
for i, (original, recovered) in enumerate(zip(steps, deserialized.steps)):
assert recovered['action'] == original['action'], \
f"Step {i}: action mismatch ({recovered['action']} != {original['action']})"
assert recovered['order'] == original['order'], \
f"Step {i}: order mismatch ({recovered['order']} != {original['order']})"
assert recovered['params'] == original['params'], \
f"Step {i}: params mismatch ({recovered['params']} != {original['params']})"
```
**Step 3: Run test and verify**
```bash
# Run property test
pytest backend/tests/property_tests/test_workflow_properties.py::test_workflow_serialization_roundtrip -v
# Hypothesis will:
# 1. Generate 100+ random examples (by default)
# 2. Shrink counterexample to minimal case (if invariant violated)
# 3. Print minimal failing input (for bug filing)
# Example output on failure:
# Falsifying example:
# test_workflow_serialization_roundtrip(
# steps=[
# {'action': 'create_agent', 'params': {}, 'order': 0},
# {'action': 'execute_workflow', 'params': {'name': ''}, 'order': 1}
# ]
# )
# Shrunk from 53 steps to 2 steps in 0.05s
```
**Step 4: Handle invariant violations**
```python
# If test fails, Hypothesis provides minimal counterexample
# Example: Steps with empty 'name' param cause serialization error
# Fix bug or refine invariant
# Option 1: Fix bug in serialization logic
# Option 2: Add precondition: 'name' must be non-empty
# Option 3: Update invariant to handle empty names correctly
# Example: Add precondition to strategy
@given(st.lists(
st.fixed_dictionaries({
'action': st.sampled_from(['create_agent', 'execute_workflow', 'present_canvas']),
'params': st.dictionaries(
st.text(min_size=1, max_size=10), # Non-empty keys
st.text(min_size=1) # Non-empty values (precondition)
),
'order': st.integers(min_value=0, max_value=1000)
}),
min_size=0,
max_size=100
))
@settings(DEFAULT_PROFILE)
def test_workflow_serialization_roundtrip_with_preconditions(steps):
"""
Test that workflow serialization is lossless for all step lists.
Invariant: For any list of workflow steps with non-empty params,
serializing and deserializing produces an equivalent workflow.
Precondition: All param keys and values must be non-empty strings.
"""
# ... same test logic
```
## Expected Behavior
**Invariant holds (test passes):**
- All generated examples satisfy invariant
- Hypothesis runs 100-200 examples (depending on profile)
- No counterexamples found
- Test completes in <30s (per TQ-03)
**Invariant violated (test fails):**
- Hypothesis finds counterexample
- Automatically shrinks to minimal failing case
- Prints minimal input that violates invariant
- Provides reproduction script
**Example failure output:**
```python
# ==================== FAILURES ====================
# ____________________ test_workflow_serialization_roundtrip ____________________
#
# Falsifying example:
# test_workflow_serialization_roundtrip(
# steps=[
# {'action': 'execute_workflow', 'params': {'name': ''}, 'order': 0}
# ]
# )
# Shrunk from 47 steps to 1 step in 0.03s
#
# assert 1 == 0
# + where 1 = len([{'action': 'execute_workflow', 'params': {}, 'order': 0}])
# + and 0 = len([])
#
# Step 0: params mismatch ({'name': ''} != {})
```
**Hypothesis shrinking process:**
1. Find first failing example (may be complex: 53 steps)
2. Simplify example (remove steps, reduce values)
3. Find minimal counterexample (2 steps → 1 step)
4. Report minimal failing case for debugging
## Bug Filing
**Automatic bug filing on invariant violation:**
```python
from tests.bug_discovery.bug_filing_service import BugFilingService
@pytest.mark.property
@given(st.lists(st.integers(), min_size=0, max_size=100))
@settings(DEFAULT_PROFILE)
def test_[invariant_name](inputs):
"""
Test that [invariant] holds for all [inputs].
Invariant: [statement]
Strategy: [strategy description]
Fails on: [known counterexample]
"""
try:
# Test logic
result = [function_under_test](inputs)
assert [invariant_check](result), f"Invariant violated: {result}"
except AssertionError as e:
# File bug with counterexample
BugFilingService.file_bug(
test_name=f"test_{[invariant_name]}_violation",
error_message=f"Invariant violation: {str(e)}",
metadata={
"test_type": "property",
"invariant": "[invariant_name]",
"counterexample": str(inputs),
"shrunk_input": str(inputs), # Hypothesis already shrunk
"hypothesis_examples": 100, # Number of examples run
"strategy": "st.lists(st.integers(), min_size=0, max_size=100)"
},
expected_behavior=f"Invariant should hold: {[invariant_statement]}",
actual_behavior=f"Invariant violated for input: {inputs}"
)
raise # Re-raise to fail test
```
**Manual bug filing (if not automatic):**
```bash
# Bug title: [Bug] Invariant violation: [Invariant Name]
# Bug body:
## Bug Description
Property-based test discovered invariant violation in [function_name].
## Invariant
**Statement:** [invariant statement]
**Domain:** [input space]
**Preconditions:** [required conditions]
## Counterexample
```python
# Minimal failing input (shrunk by Hypothesis)
inputs = [paste counterexample from test output]
# Reproducer
from backend.core.[module] import [function_name]
result = [function_name](inputs)
# Expected: [expected behavior]
# Actual: [actual behavior]
```
## Steps to Reproduce
1. Run property test: `pytest backend/tests/property_tests/test_[module]_properties.py::test_[invariant_name] -v`
2. Hypothesis finds counterexample after N examples
3. Counterexample shrunk to minimal case: [paste input]
4. Invariant violated: [description of violation]
## Shrinking Process
- Original failing example: [N] steps/items
- Shrunk to: [M] steps/items (minimal case)
- Shrinking time: [seconds]
## Hypothesis Output
```
[paste Hypothesis output with counterexample]
```
## Expected Behavior
Invariant should hold: [invariant statement]
For input: [counterexample], expected: [expected result]
## Actual Behavior
Invariant violated: [description of violation]
For input: [counterexample], actual: [actual result]
## Test Context
- **Test:** `test_[invariant_name]`
- **Hypothesis examples run:** [N]
- **Strategy:** [Hypothesis strategy used]
- **Settings:** [max_examples, deadline]
- **Platform:** [output of `uname -a`]
- **Python:** [output of `python --version`]
```
## TQ Compliance
**TQ-01 (Test Independence):**
- Each test generates fresh inputs (Hypothesis @given decorator)
- No shared state between property tests
- Each invariant tested independently
**TQ-02 (Pass Rate):**
- Property tests have 100% pass rate (invariant violations = real bugs)
- Same input always produces same output (deterministic target function)
- No flaky tests (Hypothesis provides reproducible examples)
**TQ-03 (Performance):**
- Hypothesis settings enforce deadline (30s default)
- CI profile: 50 examples, 5s deadline (fast for PR checks)
- Default profile: 200 examples, 30s deadline (thorough for local)
**TQ-04 (Determinism):**
- Same input produces same output (deterministic target function required)
- Hypothesis uses fixed random seed (reproducible examples)
- Counterexamples are reproducible (same test run = same failure)
**TQ-05 (Coverage Quality):**
- Tests invariant (observable behavior), not implementation
- Hypothesis explores input space systematically (edge cases discovered)
- Property-based: tests general property, not specific examples
## pytest.ini Marker
Add to `backend/pytest.ini`:
```ini
[pytest]
markers =
property: Property-based tests (Hypothesis, slow, thorough)
```
Run only property tests:
```bash
pytest backend/tests/property_tests/ -v -m property
```
Skip property tests in fast CI:
```bash
pytest backend/tests/ -v -m "not property"
```
## Invariant-First Thinking
**Process:**
1. **Document invariant first** (before writing test)
2. Write test that validates invariant
3. Run test to discover counterexamples
4. Fix bugs or refine invariant (add preconditions)
5. Re-run test to verify fix
**Why invariant-first?**
- Forces clarity about what must be true
- Prevents implementation-driven tests
- Catches edge cases early
- Makes tests maintainable (invariant is documentation)
**Bad example (not invariant-first):**
```python
# BAD: Test specific examples, no invariant documented
def test_workflow_serialization():
workflow = Workflow(steps=[{'action': 'create_agent'}])
serialized = workflow.serialize()
deserialized = Workflow.deserialize(serialized)
assert deserialized.steps == workflow.steps
```
**Good example (invariant-first):**
```python
# GOOD: Invariant documented, tested for all inputs
@given(st.lists(st.builds(WorkflowStep)))
@settings(DEFAULT_PROFILE)
def test_workflow_serialization_roundtrip(steps):
"""
Test that workflow serialization is lossless for all step lists.
Invariant: For any list of workflow steps, serializing and
deserializing produces an equivalent workflow.
"""
workflow = Workflow(steps=steps)
serialized = workflow.serialize()
deserialized = Workflow.deserialize(serialized)
assert deserialized.steps == workflow.steps
```
## See Also
- [Hypothesis Documentation](https://hypothesis.readthedocs.io)
- [Property-Based Testing](https://hypothesis.works/articles/what-is-property-based-testing/)
- `backend/docs/TEST_QUALITY_STANDARDS.md` - TQ-01 through TQ-05
- `backend/tests/bug_discovery/TEMPLATES/README.md` - Template usage guide