Contract Testing in .NET — PactNet Enum Removal Failures
A silent enum removal broke PayPal payments across services.
- Consumer-Driven Contracts (CDC) let each consumer define its API expectations as a pact file
- PactNet generates pact files from consumer-side tests, then provider verifies them independently
- Provider states simulate exact conditions (e.g. 'user exists') during verification
- A pact mismatch fails the provider build, not the consumer — breaking the cycle early
- Biggest mistake: treating pacts as static specs instead of versioned, evolving contracts
- Key rule: pact files must be shared via a broker or repo — never email or wiki
Imagine you order a custom-made key from a locksmith to fit your front door. The locksmith makes the key based on a mould (the contract) you gave them — so when you pick it up, it fits perfectly without you needing to be there while they cut it. Contract testing works the same way: the team who uses an API writes down exactly what they expect it to return (the contract), and the team who owns the API runs tests against that contract independently. No more 'it worked on my machine' surprises when services talk to each other.
In a microservices architecture, the scariest failures aren't the ones your unit tests catch — they're the silent ones that only blow up in production when Service A calls Service B and gets back a response shape it never expected. A field gets renamed, a nullable becomes required, an enum value disappears. Your 2000-unit-test suite is all green. Your integration environment is down for maintenance. And at 2am, your on-call phone rings. Contract testing exists precisely to close this gap.
The problem is subtle: traditional integration tests force you to spin up every dependent service simultaneously, coordinate deployment windows, and pray the test data lines up. That's slow, brittle, and it couples your CI pipelines together in ways that feel manageable on day one and catastrophic at scale. Consumer-Driven Contract (CDC) testing flips the model — each consumer declares what it needs, each provider proves it can satisfy those needs, and they do it asynchronously, independently, and fast.
By the end of this article you'll know how to implement full consumer-driven contract testing in .NET using PactNet, understand the internals of how pact files are generated and verified, handle edge cases like optional fields and provider states, integrate pact verification into your CI/CD pipeline, and avoid the production gotchas that catch even experienced teams off guard.
What is Contract Testing in .NET?
Contract testing in .NET is about defining a formal agreement between a service consumer (e.g., an API client) and a provider (e.g., a REST API). The agreement, called a pact, describes the exact request the consumer will make and the exact response it expects. With PactNet—the .NET implementation of the Pact framework—you write these expectations as unit tests in the consumer project.
The key insight: the consumer's tests generate a JSON file (the pact). The provider then runs its own tests that replay those expectations against the real provider server. No need to deploy both services together, no fragile integration environment.
Let's see a concrete example. Suppose we have an Orders API that returns order details. The consumer (a web frontend) expects a GET /orders/1 to return a 200 with body { id: 1, status: "shipped" }. The consumer test in PactNet builds that expectation.
Consumer-Driven Contract Testing with PactNet
The consumer-driven approach means the consumer dictates what it needs from the API. The provider doesn't decide the contract unilaterally. This sounds backward, but it's the only way to catch breaking changes before they reach production.
In practice, the consumer team writes PactNet tests that define every interaction they rely on. For each interaction, they specify: - The HTTP method and path - Query parameters, headers, and body (if any) - Expected response status, headers, and body structure - Provider states — named conditions the provider must set up (e.g., "order exists")
Once all consumer tests pass, the pact file is published to a Pact Broker (or stored in a shared artifact repository). The provider's CI pipeline then runs verification tests against that pact. If the provider breaks a contract, the provider build fails — not the consumer's. This shifts the failure left, before the change is ever deployed.
- The consumer defines what the API should do (like a specification).
- The provider must implement to that specification — not the other way around.
- If the provider changes the API, the consumer's pact will catch it in the provider's CI.
- This inverts the usual testing flow: the downstream team protects itself.
Provider Verification with PactNet
Now the provider team sets up verification tests. They don't need to write test scenarios — they simply load the pact file and replay it against a running instance of their API. PactNet's verification engine calls each endpoint with the exact request from the pact and checks the response matches expectations.
The provider must register provider states — methods that set up the exact data conditions required by the consumer tests. For example, if the consumer expects "order 1 exists", the provider must have a method that inserts an order with ID 1 into the test database.
Here's how to write provider verification in .NET:
Handling Provider States and Edge Cases
Provider states are the trickiest part of PactNet. They let the consumer describe preconditions without knowing how the provider sets them up. Common edge cases include:
- Missing state: the consumer says "order exists" but the provider hasn't registered that state. Verification fails.
- State with dynamic data: the consumer expects a specific ID, but the provider's test data generates random IDs. The consumer must use a fixed ID.
- Multiple states per interaction: some interactions need multiple conditions (e.g., "user is logged in" AND "order exists"). PactNet supports combining states as an array.
- Teardown: after each state, the provider should clean up any data to avoid test pollution. Use teardown endpoint.
Here's how to handle a consumer test that uses multiple states:
CI/CD Integration and Production Gotchas
The real value of contract testing comes from integrating it into your CI/CD pipelines. Both consumer and provider pipelines must run pact tests on every pull request. The typical flow:
- Consumer PR: run pact tests, publish pact file to broker.
- Provider PR: fetch latest pacts from broker, verify all interactions.
- Provider can only merge if all pacts pass.
- Consumer can deploy independently — the broker tells them which provider versions are safe.
PactNet supports publishing to a Pact Broker. The broker stores all versions of all pacts and can verify compatibility matrix.
- Missing broker authentication: use API tokens, never hardcode secrets.
- Pact file versioning: always tag pacts with the consumer version (git commit SHA). The broker uses version tags to maintain history.
- Race condition in CI: if consumer publishes pact after provider starts verification, provider might use stale pact. Use webhooks or scheduled verification.
- Pact file not published: consumer test may pass locally but publishing step may be missing in CI.
- Every pact version is tagged with consumer version, branch, and environment.
- Provider verification results are recorded against each pact version.
- You can query the broker: 'Which version of OrderApi is verified against WebFrontend v1.0?'
- This enables safe deployment — deploy only when compatible versions exist.
When Pact Verification Fails: Real Debugging Patterns
Even with well-written pacts, verification failures happen. Here's how to debug the most common causes:
1. Response field mismatch: PactNet compares actual response against expected using structural tolerance by default. If a field is missing or has wrong type, the verifier reports the exact path. Check the verifier log at debug level for details.
2. Dynamic fields: If your API returns timestamps or GUIDs, the pact must use a matcher (like PactNet's Match.Type or Match.Regex) to say 'I expect a string of this pattern, not an exact value'.
3. Query parameter order: Some HTTP clients order query parameters alphabetically. If your consumer sends them in a different order, the pact request will differ. Use List matching or ignore order.
4. Provider state setup failure: If the provider state method throws an exception, the verification stops. Check that the state name matches exactly (case-sensitive) and that all dependencies (e.g., test database) are available.
5. Missing headers: PactNet expects exact header values. If your provider adds additional headers (like Server or X-Request-Id), the verifier will fail. Use 'header blacklist' or match headers loosely.
The Silent Enum Removal That Brought Down Payments
- A contract must be machine-readable and verified every deployment.
- Never rely on documentation or manual communication for API changes.
- Every consumer must declare exactly what it expects, including enum values, optional fields, and response codes.
Key takeaways
Common mistakes to avoid
4 patternsUsing exact values for dynamic fields
Not treating pact files as versioned artifacts
Missing provider state teardown
Ignoring optional fields
Interview Questions on This Topic
How does PactNet differ from traditional integration testing for microservices?
Frequently Asked Questions
That's Testing. Mark it forged?
5 min read · try the examples if you haven't