Proof · Engineering practice
What is integration testing, and how do I verify an interface?
Integration testing checks components working together across their real boundary. State the interface contract, wire the components, exercise both success and failure paths, and retain the exchange and observed result. Passing unit tests on each side do not show what happened between them.
Test the stdio session, not only its two parts
Illustrative interface INT-REQ-005: an agent and server must complete their stdio handshake before accepting work. A test should launch the actual processes or a representative integrated fixture, send the request and capture both streams.
| Exchange | Expected observation | Evidence to retain |
|---|---|---|
| Client sends initialization request with id 1 | Server responds with matching id and compatible capabilities | Raw request/response frames, process revisions and configuration. |
| Client completes initialization, then requests work | Server accepts the request in the established session | Response and resulting operation state. |
| Server exits before completing the handshake | Client reports connection failure; pending work is not reported successful | Exit status, stderr, client result and timeout duration. |
These are proposed scenarios, not observed product results. A usable run record replaces each expected observation with the actual frames, timing and outcome, including failures. If the server never received the request, an isolated parser test cannot fill that gap.
A minimal exchange you can rerun
This Python teaching fixture was executed on 2 October 2026. It launches a separate server process, passes a JSON request over stdin and checks the reply and exit status. It demonstrates a real process boundary, not the production agent/server protocol or the full INT-REQ-005 contract.
import json, subprocess, sys
server = """import json, sys
request = json.loads(sys.stdin.readline())
print(json.dumps({"id": request["id"], "result": {"ready": True}}), flush=True)
"""
request = json.dumps({"id": 1, "method": "initialize"}) + "\n"
run = subprocess.run([sys.executable, "-c", server], input=request,
capture_output=True, text=True, timeout=2)
assert run.returncode == 0
assert json.loads(run.stdout) == {"id": 1, "result": {"ready": True}}
early = subprocess.run(
[sys.executable, "-c",
'import sys; sys.stderr.write("stopped before reply\\n"); sys.exit(3)'],
input=request, capture_output=True, text=True, timeout=2)
assert early.returncode == 3 and early.stdout == ""
The observed normal request was {"id": 1, "method": "initialize"}; the reply was {"id": 1, "result": {"ready": true}}, with exit status 0. The early-exit fixture produced no reply, returned status 3 and wrote stopped before reply to stderr. Both assertions passed.
This small test checks transport and failure observation only. It does not establish production capability negotiation, timeouts, cleanup or work dispatch. Those need the real components and the additional scenarios above.
Link the integration test without confusing it with the run
// INT-REQ-005:integration:integration
func TestMCPServerHonorsStdioProtocol(t *testing.T) {
// Start client and server, capture the exchange, assert both outcomes.
}
proof audit --check integration_evidence_witnessed
proof audit --check tests_pass
The witness check looks for a direct integration annotation on a test artifact for the active interface requirement. It does not launch the stack or inspect actual message semantics. A child’s unit-test annotation does not satisfy this interface-level relationship. Review the annotated test and the execution separately.
The configured test runner must start the relevant integration fixture and record the outcome. Testcontainers, contract-testing tools and application runners may supply parts of that setup. Their usefulness depends on which real boundary and failure modes they exercise.
Review changed endpoints and retain incomplete work
proof audit --check interface_staleness_clean
This check compares linked implementation content with the fingerprints recorded at interface review. A changed endpoint can require renewed review and evidence. Legacy reviews without fingerprints need attention because the old review alone cannot detect content drift.
If no test exists, a tracked integration deferral records the reason and follow-up. Ordinary deferrals are counted warnings, not successful exchanges. A reviewed interface, an annotated test and a passing run are three different records.
A warning becomes a merge condition only through policy
integration_evidence_witnessed emits warnings and is exempt from promotion by audit.evidence_strict. A warning can still cause a nonzero audit exit under --fail-level warn; inspect the effective configuration rather than assuming a warning is harmless.
To enforce this in CI, require the job on the candidate revision, retain the result, restrict bypasses and govern changes to the evidence policy. The interface decision may need an authorized reviewer, while execution can remain automated.
Continue with reporting the observed exchange and renewing evidence after a change.