TypeScript

Contract Testing com TypeScript: Pact e Verificação de APIs: Do Básico ao Avançado

14 min de leitura

Contract Testing com TypeScript: Pact e Verificação de APIs: Do Básico ao Avançado

O que é Contract Testing? Contract Testing é uma abordagem de testes que valida a comunicação entre dois serviços sem necessidade de integrá-los completamente. Diferente dos testes de integração tradicionais, que dependem de ambos os serviços rodando em sincronismo, o contract testing estabelece um "contrato" — uma especificação clara do que cada serviço espera receber e o que promete devolver. O contrato é como um acordo legal: se o consumidor (cliente) espera um JSON com campos específicos e o produtor (servidor) promete fornecer exatamente isso, ambos devem honrar esse compromisso. Pact é a ferramenta que nos permite definir, registrar e verificar esses contratos de forma automatizada. Isso reduz drasticamente o acoplamento entre serviços e permite que equipes trabalhem independentemente, sabendo que seus contratos serão respeitados. Por que não usar testes de integração clássicos? Testes de integração tradicionais requerem que ambos os serviços estejam disponíveis e funcionando corretamente durante a execução. Isso gera problemas: lentidão nos pipelines, dependência de infraestrutura complexa,

<h2>O que é Contract Testing?</h2>

<p>Contract Testing é uma abordagem de testes que valida a comunicação entre dois serviços sem necessidade de integrá-los completamente. Diferente dos testes de integração tradicionais, que dependem de ambos os serviços rodando em sincronismo, o contract testing estabelece um &quot;contrato&quot; — uma especificação clara do que cada serviço espera receber e o que promete devolver.</p>

<p>O contrato é como um acordo legal: se o consumidor (cliente) espera um JSON com campos específicos e o produtor (servidor) promete fornecer exatamente isso, ambos devem honrar esse compromisso. Pact é a ferramenta que nos permite definir, registrar e verificar esses contratos de forma automatizada. Isso reduz drasticamente o acoplamento entre serviços e permite que equipes trabalhem independentemente, sabendo que seus contratos serão respeitados.</p>

<h3>Por que não usar testes de integração clássicos?</h3>

<p>Testes de integração tradicionais requerem que ambos os serviços estejam disponíveis e funcionando corretamente durante a execução. Isso gera problemas: lentidão nos pipelines, dependência de infraestrutura complexa, dificuldade em isolar falhas e impossibilidade de testar serviços que ainda não foram desenvolvidos. Contract testing quebra esse problema: você simula o comportamento esperado e verifica se ambos os lados respeitam o contrato.</p>

<h2>Introdução ao Pact e sua arquitetura</h2>

<p>Pact é uma ferramenta open-source que implementa contract testing através de interações gravadas entre consumidor e provedor. A ideia central é simples: o consumidor define o que espera receber do provedor, e o provedor verifica se realmente fornece isso. Essas expectativas são gravadas em um arquivo JSON chamado &quot;pact file&quot;, que serve como fonte da verdade.</p>

<p>A arquitetura do Pact funciona em duas fases: <strong>fase de testes no consumidor</strong> e <strong>fase de verificação no provedor</strong>. Durante a fase do consumidor, você define o comportamento esperado das chamadas HTTP. O Pact coloca um servidor mock no lugar do serviço real, registra as interações e as salva em um arquivo. Depois, durante a fase do provedor, esse arquivo é executado contra o serviço real para garantir que ele honra o contrato.</p>

<h3>Instalação e configuração inicial</h3>

<p>Para começar com Pact em TypeScript, você precisa instalar as dependências corretas. O ecossistema Pact é rico, mas para TypeScript usaremos <code>@pact-foundation/pact</code>, que é mantido pela equipe oficial.</p>

<pre><code class="language-bash">npm install --save-dev @pact-foundation/pact jest @types/jest ts-jest

npm install --save-dev typescript</code></pre>

<p>Crie um arquivo <code>jest.config.js</code> para configurar o TypeScript:</p>

<pre><code class="language-javascript">module.exports = {

preset: &#039;ts-jest&#039;,

testEnvironment: &#039;node&#039;,

testMatch: [&#039;/__tests__//*.test.ts&#039;],

moduleFileExtensions: [&#039;ts&#039;, &#039;js&#039;]

};</code></pre>

<p>E um <code>tsconfig.json</code> básico:</p>

<pre><code class="language-json">{

&quot;compilerOptions&quot;: {

&quot;target&quot;: &quot;ES2020&quot;,

&quot;module&quot;: &quot;commonjs&quot;,

&quot;lib&quot;: [&quot;ES2020&quot;],

&quot;outDir&quot;: &quot;./dist&quot;,

&quot;rootDir&quot;: &quot;./src&quot;,

&quot;strict&quot;: true,

&quot;esModuleInterop&quot;: true,

&quot;skipLibCheck&quot;: true,

&quot;forceConsistentCasingInFileNames&quot;: true

}

}</code></pre>

<h2>Testando o Consumidor com Pact</h2>

<p>Na perspectiva do consumidor, você é o código cliente que depende de um serviço externo. Seu trabalho é definir exatamente o que você espera desse serviço: quais endpoints você chamará, que métodos HTTP usará, que corpo você enviará e que respostas espera receber.</p>

<h3>Estruturando um teste de consumidor</h3>

<p>Vamos criar um exemplo prático: imagine um serviço de usuários que precisa consultar dados em um serviço de perfil. O consumidor fará requisições GET para <code>/api/users/{id}</code> e espera um JSON com nome, email e idade.</p>

<pre><code class="language-typescript">import { Pact, Interaction } from &#039;@pact-foundation/pact&#039;;

import axios from &#039;axios&#039;;

// Classe que representa o cliente do serviço de perfil

class UserProfileClient {

constructor(private baseUrl: string) {}

async getUserProfile(userId: number) {

const response = await axios.get(${this.baseUrl}/api/users/${userId});

return response.data;

}

}

describe(&#039;UserProfileClient - Consumer Tests&#039;, () =&gt; {

const provider = new Pact({

consumer: &#039;UserService&#039;,

provider: &#039;ProfileService&#039;,

port: 8081,

logLevel: &#039;warn&#039;

});

beforeAll(() =&gt; provider.setup());

afterEach(() =&gt; provider.verify());

afterAll(() =&gt; provider.finalize());

it(&#039;should retrieve user profile by ID&#039;, async () =&gt; {

// Define a interação esperada

await provider.addInteraction({

state: &#039;user 123 exists&#039;,

uponReceiving: &#039;a request for user profile&#039;,

withRequest: {

method: &#039;GET&#039;,

path: &#039;/api/users/123&#039;

},

willRespondWith: {

status: 200,

body: {

id: 123,

name: &#039;João Silva&#039;,

email: &#039;joao@example.com&#039;,

age: 28

}

}

});

// Instancia o cliente apontando para o mock do Pact

const client = new UserProfileClient(&#039;http://localhost:8081&#039;);

// Faz a chamada

const profile = await client.getUserProfile(123);

// Valida a resposta

expect(profile).toEqual({

id: 123,

name: &#039;João Silva&#039;,

email: &#039;joao@example.com&#039;,

age: 28

});

});

it(&#039;should handle user not found&#039;, async () =&gt; {

await provider.addInteraction({

state: &#039;user 999 does not exist&#039;,

uponReceiving: &#039;a request for non-existent user&#039;,

withRequest: {

method: &#039;GET&#039;,

path: &#039;/api/users/999&#039;

},

willRespondWith: {

status: 404,

body: {

error: &#039;User not found&#039;

}

}

});

const client = new UserProfileClient(&#039;http://localhost:8081&#039;);

try {

await client.getUserProfile(999);

fail(&#039;Should have thrown an error&#039;);

} catch (error: any) {

expect(error.response.status).toBe(404);

expect(error.response.data.error).toBe(&#039;User not found&#039;);

}

});

});</code></pre>

<h3>Entendendo as interações</h3>

<p>Cada interação no Pact possui quatro componentes essenciais: um <strong>estado</strong> (state), uma descrição do que está sendo requisitado, a <strong>requisição esperada</strong> e a <strong>resposta esperada</strong>. O estado é importante porque permite testar diferentes cenários — quando o usuário existe, quando não existe, quando há erro no servidor, etc.</p>

<p>A requisição define o método HTTP, o caminho, headers e body (se aplicável). A resposta define o código de status e o corpo da resposta. O Pact grava tudo isso e cria um arquivo chamado <code>pact/UserService-ProfileService.json</code> que será usado posteriormente para validar o servidor.</p>

<h2>Verificando o Contrato no Provedor</h2>

<p>Depois que os testes do consumidor rodam com sucesso, você tem um arquivo pact que descreve o contrato. Agora é hora de verificar se o provedor (o servidor real) honra esse contrato. Isso é feito através de &quot;testes de verificação&quot; que executam cada interação contra a API real.</p>

<h3>Implementando o provedor</h3>

<p>Primeiro, vamos criar um servidor Express simples que simula o serviço de perfil:</p>

<pre><code class="language-typescript">import express, { Express } from &#039;express&#039;;

export function createProfileServer(): Express {

const app = express();

app.use(express.json());

// Mock database

const users = {

123: { id: 123, name: &#039;João Silva&#039;, email: &#039;joao@example.com&#039;, age: 28 },

456: { id: 456, name: &#039;Maria Santos&#039;, email: &#039;maria@example.com&#039;, age: 32 }

};

app.get(&#039;/api/users/:id&#039;, (req, res) =&gt; {

const userId = parseInt(req.params.id);

const user = users[userId as keyof typeof users];

if (user) {

res.json(user);

} else {

res.status(404).json({ error: &#039;User not found&#039; });

}

});

return app;

}</code></pre>

<h3>Escrevendo testes de verificação</h3>

<p>Os testes de verificação são diferentes: você não define interações, apenas verifica se o provedor cumpre as definidas pelo consumidor. O Pact lê o arquivo gerado anteriormente e executa cada interação contra seu servidor real.</p>

<pre><code class="language-typescript">import { Verifier } from &#039;@pact-foundation/pact&#039;;

import { createProfileServer } from &#039;../server&#039;;

describe(&#039;ProfileService - Provider Verification&#039;, () =&gt; {

let server: any;

const port = 3000;

beforeAll(async () =&gt; {

const app = createProfileServer();

server = app.listen(port);

});

afterAll(() =&gt; {

server.close();

});

it(&#039;validates the pact with the consumer&#039;, async () =&gt; {

const verifier = new Verifier({

providerBaseUrl: http://localhost:${port},

pactFiles: [&#039;./pact/UserService-ProfileService.json&#039;],

providerVersion: &#039;1.0.0&#039;

});

await verifier.verifyProvider();

});

});</code></pre>

<p>Quando este teste rodas, o Pact automaticamente:</p>

<ol>

<li>Lê o arquivo pact gerado pelo consumidor</li>

<li>Extrai cada interação (requisição esperada)</li>

<li>Faz a requisição contra seu servidor real</li>

<li>Compara a resposta real com a resposta esperada</li>

<li>Relata se há divergências</li>

</ol>

<p>Se o servidor responder diferente do esperado, o teste falha imediatamente, alertando que o contrato foi quebrado.</p>

<h3>Gerenciando estados do provedor</h3>

<p>Em cenários mais complexos, você pode precisar preparar o provedor em diferentes estados. O Pact suporta &quot;provider states&quot; — funções que preparam o banco de dados ou o estado da aplicação antes de executar cada interação.</p>

<pre><code class="language-typescript">import { Verifier } from &#039;@pact-foundation/pact&#039;;

describe(&#039;ProfileService - Provider Verification com States&#039;, () =&gt; {

let server: any;

const port = 3000;

beforeAll(async () =&gt; {

const app = createProfileServer();

server = app.listen(port);

});

afterAll(() =&gt; {

server.close();

});

it(&#039;validates pact with provider states&#039;, async () =&gt; {

const verifier = new Verifier({

providerBaseUrl: http://localhost:${port},

pactFiles: [&#039;./pact/UserService-ProfileService.json&#039;],

providerVersion: &#039;1.0.0&#039;,

stateHandlers: {

&#039;user 123 exists&#039;: async () =&gt; {

// Garante que o usuário 123 existe no banco

console.log(&#039;Preparando estado: user 123 exists&#039;);

},

&#039;user 999 does not exist&#039;: async () =&gt; {

// Garante que o usuário 999 não existe

console.log(&#039;Preparando estado: user 999 does not exist&#039;);

}

}

});

await verifier.verifyProvider();

});

});</code></pre>

<h2>Fluxo de Trabalho Completo e Boas Práticas</h2>

<p>Um fluxo de contract testing maduro envolve múltiplos passos e considerações importantes. Não é apenas escrever testes — é sobre estabelecer uma cultura de contratos e verificação contínua.</p>

<h3>Estrutura de diretórios recomendada</h3>

<pre><code>projeto/

├── src/

│ ├── clients/

│ │ └── profileClient.ts

│ ├── server.ts

│ └── services/

├── __tests__/

│ ├── consumer/

│ │ └── profileClient.test.ts

│ └── provider/

│ └── profileService.test.ts

├── pact/

│ └── UserService-ProfileService.json

└── jest.config.js</code></pre>

<h3>Executando e publicando contratos</h3>

<p>Depois que seus testes rodam, você provavelmente quer compartilhar os contratos com a equipe do provedor. Pact oferece um &quot;Broker&quot; — um repositório central onde contratos podem ser publicados e acessados. Isso é especialmente útil em arquiteturas de microsserviços com múltiplas equipes.</p>

<pre><code class="language-bash"># Publicar contrato no Pact Broker

npm run test:consumer

npx pact publish ./pact \

--consumer-app-version=1.0.0 \

--broker-url=https://seu-broker.pact.sh \

--broker-token=seu-token</code></pre>

<h3>Validações importantes no contrato</h3>

<p>Um contrato bem escrito é específico, mas não frágil. Evite capturar detalhes que mudarão frequentemente. Use matchers do Pact para definir padrões em vez de valores exatos quando apropriado:</p>

<pre><code class="language-typescript">import { Matchers } from &#039;@pact-foundation/pact&#039;;

await provider.addInteraction({

state: &#039;user 123 exists&#039;,

uponReceiving: &#039;a request for user profile&#039;,

withRequest: {

method: &#039;GET&#039;,

path: &#039;/api/users/123&#039;

},

willRespondWith: {

status: 200,

body: {

id: Matchers.number(123),

name: Matchers.string(&#039;João Silva&#039;),

email: Matchers.regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, &#039;joao@example.com&#039;),

age: Matchers.number(28)

}

}

});</code></pre>

<p>Matchers permitem validar formatos sem capturar valores específicos. Um regex para email, por exemplo, valida que qualquer email em formato correto será aceito, não apenas &quot;joao@example.com&quot;.</p>

<h3>Integração com CI/CD</h3>

<p>Em um pipeline de continuous integration, você quer rodar testes de consumidor e provedor em paralelo, sempre que código for alterado:</p>

<pre><code class="language-yaml"># .github/workflows/contract-tests.yml

name: Contract Tests

on: [push, pull_request]

jobs:

consumer:

runs-on: ubuntu-latest

steps:

  • uses: actions/checkout@v3
  • uses: actions/setup-node@v3

with:

node-version: &#039;18&#039;

  • run: npm install
  • run: npm run test:consumer
  • name: Publish pact

if: success()

run: npm run pact:publish

provider:

runs-on: ubuntu-latest

needs: consumer

steps:

  • uses: actions/checkout@v3
  • uses: actions/setup-node@v3

with:

node-version: &#039;18&#039;

  • run: npm install
  • run: npm run test:provider</code></pre>

<h2>Conclusão</h2>

<p>Contract testing com Pact e TypeScript oferece três benefícios fundamentais que transformam como você trabalha com APIs. Primeiro, <strong>desacopla equipes</strong>: consumidores e provedores podem desenvolver independentemente, confiando apenas no contrato, sem esperar por serviços prontos. Segundo, <strong>acelera feedback</strong>: ao contrário de testes de integração que requerem infraestrutura complexa, contract tests rodam rapidamente e isoladamente em qualquer máquina. Terceiro, <strong>documenta expectativas</strong>: o arquivo pact é documentação viva que sempre reflete a realidade de como dois serviços se comunicam.</p>

<p>O conhecimento prático que você ganhou aqui — estruturar interações no consumidor, implementar verificações no provedor, usar matchers para tornar contratos resilientes — é suficiente para iniciar em projetos reais. A partir daqui, explore integração com Pact Broker para arquiteturas maiores e considere estender esse padrão para todos os seus consumidores de API.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://docs.pact.foundation/" target="_blank" rel="noopener noreferrer">Documentação Oficial do Pact</a></li>

<li><a href="https://github.com/pact-foundation/pact-js" target="_blank" rel="noopener noreferrer">Pact JS Library - GitHub</a></li>

<li><a href="https://martinfowler.com/bliki/ContractTest.html" target="_blank" rel="noopener noreferrer">Contract Testing Guide - Martin Fowler</a></li>

<li><a href="https://docs.pact.sh/" target="_blank" rel="noopener noreferrer">Pact Broker - Compartilhamento de Contratos</a></li>

<li><a href="https://jestjs.io/docs/getting-started#using-typescript" target="_blank" rel="noopener noreferrer">TypeScript + Jest Best Practices</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Guia Completo de CLI com TypeScript: Construindo Ferramentas de Linha de Comando Tipadas
Guia Completo de CLI com TypeScript: Construindo Ferramentas de Linha de Comando Tipadas

Introdução: Por que TypeScript em CLIs? Quando você desenvolve aplicações de...

Boas Práticas de React Query com TypeScript: Queries, Mutations e Tipos Inferidos para Times Ágeis
Boas Práticas de React Query com TypeScript: Queries, Mutations e Tipos Inferidos para Times Ágeis

Por que React Query Revolucionou o Gerenciamento de Estado Durante minha carr...

O que Todo Dev Deve Saber sobre Domain-Driven Design com TypeScript: Entidades e Value Objects Tipados
O que Todo Dev Deve Saber sobre Domain-Driven Design com TypeScript: Entidades e Value Objects Tipados

Fundamentos do Domain-Driven Design Domain-Driven Design (DDD) é uma metodolo...