Post

Contract Testing: Prevent Breaking Changes Before Production

“It worked locally. Tests passed. But production still broke.”

If you’re building distributed systems with multiple services and frontends, you’ve likely encountered this (whether using .NET + Angular, Node.js + React, Python + Vue, or any combination):

  • A backend change gets deployed
  • The frontend suddenly breaks
  • No tests warned you

The issue isn’t always logic.

It’s often a broken contract between systems.

The Problem: Silent API Breakages

In a typical setup:

  • Service Provider: An API or microservice
  • Service Consumer: A frontend, app, or another service
  • Communication: JSON over HTTP (or any protocol)

Everything depends on one thing:

The consumer and provider agreeing on what data looks like

A Real Example

Let’s use a .NET API and Angular frontend as an example (though this applies to any tech stack).

API Provider (Initial Version)

1
2
3
4
5
6
public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}
1
2
3
4
5
6
7
8
9
10
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
    return Ok(new UserDto
    {
        Id = 1,
        Name = "Billy",
        Email = "[email protected]"
    });
}

Consumer Interface (Angular Example)

1
2
3
4
5
export interface User {
  id: number;
  name: string;
  email: string;
}

Everything works perfectly.

Then a “Small” Change Happens

1
2
3
4
5
6
public class UserDto
{
    public int Id { get; set; }
    public string FullName { get; set; } // renamed
    public string Email { get; set; }
}

Production Result

1
user.name //  undefined

The UI breaks.

Why Didn’t Traditional Tests Catch This?

  • Unit tests → passed (backend logic is fine)
  • Integration tests → passed (they used the old model)
  • The API still returns valid JSON

But the contract between systems changed—and traditional tests don’t verify that.

What is Contract Testing?

Contract testing ensures that your service provider always matches what the consumer expects.

A contract defines:

  • Request format
  • Response structure
  • Required fields
  • Data types

In Simple Terms

The consumer (frontend, app, or service) defines expectations The provider (API or service) must satisfy them

Consumer vs Provider

Consumer

  • Calls the service/API
  • Defines expected structure
  • Examples: Angular frontend, React app, mobile app, another microservice

Provider

  • Returns the data
  • Must not break expectations
  • Examples: .NET API, Node.js backend, Python service, GraphQL endpoint

How Contract Testing Works

Instead of relying only on integration tests:

  1. Angular defines expectations
  2. A contract file is generated
  3. .NET verifies the contract

Flow

1
2
3
4
5
6
7
Consumer Test (e.g., Angular)
      ↓
Generates Contract
      ↓
Saved as JSON/YAML
      ↓
Provider verifies against contract (e.g., .NET API)

Practical Example

(.NET backend + Angular frontend as an example)

Step 1: Define Expectations in Consumer (Angular example)

1
2
3
4
5
6
7
8
9
10
const expectedUser = {
  id: 1,
  name: "Billy",
  email: "[email protected]"
};

it("should fetch user correctly", async () => {
  const user = await userService.getUser(1);
  expect(user).toEqual(expectedUser);
});

Generated Contract (Simplified)

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "request": {
    "method": "GET",
    "path": "/api/users/1"
  },
  "response": {
    "body": {
      "id": 1,
      "name": "Billy",
      "email": "[email protected]"
    }
  }
}

Step 2: Verify in Provider (.NET API example)

Install Pact:

1
dotnet add package PactNet

Provider Test

1
2
3
4
5
6
7
8
9
10
[Fact]
public void VerifyPact()
{
    var pactVerifier = new PactVerifier();

    pactVerifier
        .ServiceProvider("UserApi", "http://localhost:5000")
        .WithFileSource(new FileInfo("pacts/userapi-angular.json"))
        .Verify();
}

If Provider Breaks the Contract

1
public string FullName { get; set; }
  • The test fails immediately

  • You catch the issue before deployment

Where Contract Testing Fits

1
2
3
4
5
6
7
8
9
10
11
E2E Tests
(user journeys)

Integration Tests
(service interactions)

Contract Tests 
(API agreements)

Unit Tests
(business logic)

Why This Matters in Real Projects

Safer Refactoring

Change DTOs, schema, or API responses without fear. Contract tests verify nothing broke.

Independent Development

Consumer and provider teams move faster. Changes are caught instantly, not in production.

Faster Debugging

Failures clearly show what broke

Stronger CI/CD Pipelines

1
2
3
Consumer Build → Generate Contract
Provider Build → Verify Contract
Deploy → Only if both pass

This works with any tech stack.

Common Mistakes

Over-Specifying Data

Bad:

1
"name": "Billy Okeyo"

Good:

1
"name": "string"

Testing Everything

Only validate fields your frontend actually uses

Ignoring Versioning

Breaking contracts without versioning leads to production issues

Best Practices

Keep Contracts Minimal

Focus only on required fields

Version Your API

1
2
/api/v1/users
/api/v2/users

Automate in CI/CD

Contracts should be generated and verified automatically

Use Realistic Data

Avoid unrealistic mocks

Final Takeaway

Unit tests verify logic Integration tests verify systems E2E tests verify user flows

Contract tests verify agreements

“Most production bugs aren’t failures… they’re misunderstandings between systems.”

Contract testing eliminates those misunderstandings.

This post is licensed under CC BY 4.0 by the author.