Testing and Release Policy
SCHEMABOUND treats testing and release discipline as part of the public contract. The goal is not just to ship code that works, but to ship public surfaces that are validated, predictable, and safe to adopt.
Test Layers
Each layer has a fixed contract. Violating that contract — mocking in integration tests, introducing I/O in unit tests, stubbing a container away in an E2E test — is a test smell, not a trade-off.
| Layer | Collaborators | Mocks? | Stubs? | Where it runs |
|---|---|---|---|---|
| Unit | In-process only | Never | Stubs are acceptable | Local, no I/O |
| Integration | Real containerized runtimes | Never | Never | Local containers (Testcontainers or Compose) |
| End-to-End | Fully deployed environment | Never | Never | Local or staging deployment |
Unit Tests
Unit tests should stay fast, deterministic, and in-process.
Expectations:
- no live RPC or HTTP calls
- no containers
- no shared ports or database paths between test cases
Stubs and mocks are not the same thing. A stub returns a fixed value to put the system under test into a known state — that is acceptable. A mock asserts that the system under test calls it in a specific way (call count, argument shapes). If a unit test needs a mock, the production dependency has a design problem. Simplify the dependency before adding mock scaffolding.
If a unit test needs a large stub or mock hierarchy, treat that as a signal to simplify the production code, not to add more scaffolding.
Test isolation. Each test that starts a server or opens a database must use a unique address and path. Use atomic counters or unique temporary paths to avoid flaky collisions:
- Unique ports per test — allocate with an atomic counter, never hardcode a shared port.
- Unique database paths — use a helper like
get_test_db_path()that returns a temp-scoped unique path per test.
Assertion style. Prefer exact value assertions over substring matching.
- Use
matches!()for enum variant checks instead of.to_string().contains(). - Avoid snapshot tests — write explicit expected values so failures are immediately actionable.
- Assert on typed values, not serialized strings, wherever the types are available.
Integration Tests
Integration tests should validate real boundaries.
Expectations:
- real network requests over the actual protocol (TCP, gRPC, HTTP)
- a runtime that actually starts and listens on a real port
- real dependencies where the boundary matters — no shortcut local clients standing in for protocol behavior
- no stubs or mocks at any layer
If a test is named integration, it must talk to a real started runtime over the network. Tests that call in-process implementations directly are unit tests, not integration tests, regardless of what they are named.
End-To-End Tests
End-to-end tests validate a deployed environment from the outside. They are the right fit for rollout, wiring, secret, and networking concerns when that environment exists.
Expectations:
- fully deployed environment, nothing replaced or stubbed
- tests drive the system only through public entry points (API, CLI, browser)
- environment lifecycle (start, seed, teardown) is explicit and automated
CI Expectations
Pull requests to main should pass the quality profile relevant to the repository being changed. That can include:
- linting and formatting
- unit and integration tests
- build validation
- documentation builds
- coverage or maintainability gates where they add real signal
- dependency and security checks
SDK-specific maintainability gates are intentionally selective. They should exist where a codebase contains enough handwritten logic to justify them, not just because a language binding exists.
Local Validation
When available, enable the repo-managed hooks locally:
make hooks-install
The local hook path is intended to catch common failures before you open a pull request. It complements CI, but does not replace it.
Release Discipline
Validation and publication are separate steps.
The expected release path is:
- merge reviewed code to
main - allow validation to complete on the merged revision
- create the appropriate release tag when publication is intended
Current public release patterns include:
public-v*for public subtree publicationsdk-python-v*for Python SDK publicationsdk-<language>-v*as the general form for future SDK release workflows
Why This Separation Exists
Separating merge validation from publication keeps the public SCHEMABOUND surface more predictable for adopters.
It ensures that:
- every published artifact passed review first
- release timing is deliberate rather than accidental
- public packages and docs stay aligned with validated source
- teams can reason about adoption risk more clearly