Skip to content
All essays
CraftMarch 27, 202611 min

Contract Testing: Ensuring Service Integration

Master contract testing and ensure reliable microservices communication

Ü
Ümit Uz
Mobile & Full Stack Developer

Contract testing verifies that services can communicate correctly. It tests API contracts without spinning up dependent services.

What is Contract Testing?

Contract testing validates that a provider service meets the expectations of consumer services. Unlike integration tests, contract tests are fast and isolated.

Pact

Consumer Side

typescript
// consumer/consumer.test.ts
import { Pact } from '@pact-foundation/pact';
import { API } from './api';

describe('API Consumer Pact', () => {
  const provider = new Pact({
    consumer: 'OrderService',
    provider: 'UserService',
    port: 1234,
    log: './logs/pact.log',
    dir: './pacts'
  });
  
  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());
  
  afterEach(() => provider.verify());
  
  test('get user by ID', async () => {
    const expectedUser = {
      id: '123',
      name: 'John Doe',
      email: 'john@example.com'
    };
    
    await provider.addInteraction({
      state: 'user exists',
      uponReceiving: 'a request for user',
      withRequest: {
        method: 'GET',
        path: '/api/users/123'
      },
      willRespondWith: {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: expectedUser
      }
    });
    
    const api = new API('http://localhost:1234');
    const user = await api.getUser('123');
    
    expect(user).toEqual(expectedUser);
  });
});

Provider Side

typescript
// provider/provider.test.ts
import { Verifier } from '@pact-foundation/pact';
import { server } from './server';

describe('API Provider Pact', () => {
  let app: any;
  
  beforeAll(async () => {
    app = await server.start();
  });
  
  afterAll(() => server.stop());
  
  test('validates consumer expectations', async () => {
    const verifier = new Verifier({
      providerBaseUrl: 'http://localhost:3000',
      pactUrls: ['./pacts/orderservice-userservice.json'],
      provider: 'UserService',
      stateHandlers: {
        'user exists': () => {
          // Set up test data
          return Promise.resolve();
        }
      }
    });
    
    await verifier.verify();
  });
});

Spring Cloud Contract

Producer

groovy
// BeerService.groovy
import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description '''
    Validates getting beer by ID
    
    '''
    request {
        method GET()
        url $(consumer(regex('/beers/[0-9]+')), producer('/beers/1'))
    }
    response {
        status 200
        body([
            id: $(consumer(1), producer(anyNumber())),
            name: $(consumer('Test Beer'), producer(anyNonEmptyString())),
            type: 'IPA'
        ])
        headers {
            contentType(applicationJson())
        }
    }
}

Consumer Test

groovy
// BeerServiceSpec.groovy
class BeerServiceSpec extends Specification {
    @AutoStubWirePort
    int stubPort
    
    def 'should get beer by id'() {
        given:
        def client = new BeerClient("localhost:${stubPort}")
        
        when:
        def beer = client.getBeer(1)
        
        then:
        beer.id == 1
        beer.name == 'Test Beer'
        beer.type == 'IPA'
    }
}

Benefits

  • Fast Tests: No need to spin up dependencies
  • Fail Fast: Contract failures caught early
  • Documentation: Contracts serve as documentation
  • Independent Development: Teams work in parallel

Best Practices

  1. 1Version your contracts
  2. 2Store contracts in git
  3. 3Run in CI/CD pipeline
  4. 4Update contracts collaboratively
  5. 5Test both consumer and provider

Conclusion

Contract testing enables confident microservices development. Catch integration issues early without slow integration tests.

Enjoyed this essay? Share it.

Related essays

Next essay
Cross-Chain Bridges: Connecting Blockchains