A data integration can keep returning HTTP 200 while quietly becoming unsafe. A producer adds a field, changes a number to a string, starts sending an empty value, or renames a status. The request succeeds. The consumer accepts the payload. The meaning changes anyway.
That is the gap data integration contract tests are designed to close. A contract test turns the shared expectations between a producer and a consumer into an executable check. It verifies the fields, types, required states, identifiers, and meanings that must remain stable before a release can move forward.
This is different from testing whether one function runs. It tests whether two systems still agree about the data crossing their boundary.
What an Integration Contract Actually Defines
A contract is a compact description of what one system promises and what another system relies on. It should be specific enough to test, but focused enough that it does not become a copy of every internal implementation detail.
For a CRM-to-email sync, a contract might define:
person_idis a stable string identifier and is required.emailis either a valid address or an explicit null, not a placeholder such asN/A.subscription_statususes a documented set of values.updated_atis an ISO 8601 timestamp with a stated timezone convention.- A deleted or suppressed record has a defined downstream action.
Notice what is missing: the producer’s database table name, the consumer’s internal class names, and other details that do not belong to the shared boundary. The contract describes observable behavior and data meaning.
Why Unit Tests Miss Integration Drift
Unit tests are valuable, but they usually protect one codebase at a time. The producer may have a passing test that confirms it can serialize a record. The consumer may have a passing test that confirms it can parse a fixture. Neither test proves that the producer’s current output still matches the consumer’s current expectation.
Integration drift often appears in the space between those tests:
- A field remains present but changes from a number to a formatted string.
- A field that used to be required becomes absent for a new record type.
- A status keeps the same name but changes its operational meaning.
- A nested object becomes a flat value, or a flat value becomes nested.
- A producer adds a new event that the consumer treats as an existing event.
- A valid response contains an identifier that no longer matches the source system’s identity rules.
These changes can pass basic request tests because the endpoint still responds. Contract tests make the boundary explicit and fail closer to the change that introduced the risk.
Choose the Contract Owner
Every contract needs an owner. In practice, there are two useful models.
Provider-owned contract
The producing system publishes a schema or representative payloads. Consumers test that their code can handle the provider’s documented output. This works well when one API serves many consumers and the provider can publish compatibility rules.
Consumer-driven contract
The consuming system records the fields and behaviors it actually needs. The producer verifies that it can continue to satisfy those expectations. This can prevent a provider from removing a field that appears unused in its own code but is essential to a downstream workflow.
The important decision is not which label sounds best. It is making ownership visible. Someone must review changes, decide whether a contract change is compatible, and coordinate migrations when it is not.
Build Useful Fixtures
A contract test is only as good as the examples it runs against. One perfect-looking fixture is not enough. Build a small set that represents the boundary conditions a real workflow must handle.
For a form-to-CRM integration, useful fixtures might include:
- A complete record with every required value.
- A record with an optional field omitted.
- A record with an explicit null where null is allowed.
- A record with a duplicate or previously seen stable ID.
- A record that contains a non-ASCII name, a long value, or a line break.
- A rejected record with a machine-readable validation error.
- A deletion, suppression, or status-change event.
Keep sensitive data out of test fixtures. Use synthetic values that preserve the shape and edge cases of production data without copying real names, emails, donor details, payment references, or survey responses into repositories or CI logs.
Test Meaning, Not Just Shape
Schema validation can confirm that a field is a string. It cannot, by itself, confirm that the string means what the consumer thinks it means.
Include semantic assertions where they matter:
- Stable identifiers do not change when a display name changes.
- A date represents the intended event time rather than the processing time.
- A suppression state prevents a message from being eligible for sending.
- An order total is represented in the documented currency and precision.
- A response marked as accepted does not imply that every downstream side effect is complete.
This is where contract testing connects to reconciliation. A response can be structurally valid and still produce the wrong business outcome. A strong test suite checks both the boundary payload and the action the consumer takes after receiving it.
Put the Test at the Release Boundary
Run contract tests before deployment, migration, or a scheduled integration change reaches production. The exact mechanics depend on the architecture, but the gate should answer a simple question: can the current producer and consumer still work together under the agreed contract?
A practical release sequence looks like this:
- The producer or consumer changes a schema, mapping, status, or transformation.
- Representative fixtures and contract tests run in a shared test environment.
- The test reports the changed field, expected state, observed state, and affected consumer.
- Compatible changes continue through review; breaking changes are blocked or versioned.
- A small canary or controlled synchronization validates the deployed path.
- Monitoring and reconciliation confirm that accepted records produced the intended result.
Do not make every change a hard failure. Additive fields may be safe for tolerant consumers and unsafe for strict ones. A new optional value may still require a reporting update. Classify changes as compatible, conditionally compatible, or breaking, then document the reasoning.
Common Contract-Test Mistakes
- Testing only the happy path. Missing, null, duplicate, rejected, deleted, and reordered states are part of the contract.
- Copying production payloads. This creates privacy and retention risk without improving test quality.
- Asserting every field. Overly strict tests make harmless additions look breaking and encourage teams to disable the suite.
- Ignoring versions. If a breaking change is necessary, publish a clear version or migration path instead of silently changing the old contract.
- Stopping at schema validation. A valid payload can still map to the wrong record or trigger the wrong side effect.
- Running tests only after deployment. A test that discovers drift in production is an incident detector, not a release gate.
A Compact Review Checklist
Before calling an integration contract ready, confirm that the team can answer:
- Which system owns each field’s meaning?
- Which identifiers are stable, and how are they matched?
- Which values are required, optional, nullable, or prohibited?
- How are dates, currencies, statuses, deletes, and suppressions represented?
- Which changes are compatible, conditional, or breaking?
- Which fixtures cover malformed, incomplete, duplicate, and edge-case records?
- Where does the contract test run, and who reviews failures?
- How will a passing contract be followed by reconciliation of the real outcome?
Contract tests give integration teams a shared place to discuss change before it becomes a data problem. They do not replace unit tests, end-to-end tests, monitoring, or reconciliation. They make the agreement between systems visible and executable, which gives each of those practices a clearer boundary.
DigitalWerks can help review an integration’s payloads, field meanings, fixtures, release gates, and reconciliation checks as one connected workflow. Contact DigitalWerks to plan a more dependable data integration.
For a related look at how an API can evolve without breaking its consumers, see API versioning and compatibility planning.