Spec-driven testing: testes de contrato a partir da spec

Spec-driven testing é a prática de derivar testes de contrato, testes de integração e fixtures direto do arquivo de especificação. Tudo isso acontece antes que exista qualquer linha da camada de aplicação.
Neste artigo, vamos entender como um agente de IA compila a spec em artefatos verificáveis. Além disso, veremos por que esse fluxo garante que o código responda à regra, e não às suposições do próprio agente.
O que é spec-driven testing
O spec-driven testing é a extensão natural do Spec Driven Development para a camada de verificação. Em vez de deixar os testes para o fim, o fluxo transforma a spec na origem de tudo que valida o sistema.
Na prática, cada regra escrita no arquivo de especificação vira, no mínimo, um caso de teste executável. Além disso, os exemplos da spec viram fixtures, e os contratos de API viram validações de schema.
A ideia não é nova, porque testes baseados em especificação existem desde a engenharia de software formal. Contudo, os agentes de codificação mudaram o peso dessa prática.
Agora existe um terceiro participante que escreve código muito rápido. Muitas vezes, esse participante também testa aquilo que ele mesmo imaginou.
Por que testar contra a regra e não contra suposições
Imagine que você pede a um agente uma funcionalidade e, depois, os testes dela. Nesse caso, ele lê o próprio código para decidir o que testar.
Dessa forma, o teste confirma o comportamento implementado, mesmo que esse comportamento esteja errado.
Esse fenômeno gera testes tautológicos, ou seja, testes que passam porque espelham a implementação. Por isso, uma cobertura de 90% pode esconder uma regra de negócio completamente ignorada.
Com spec-driven testing, a fonte do teste deixa de ser o código e passa a ser a regra. Assim, se o agente interpretar errado um requisito, o teste derivado da spec falha na hora.
Um estudo do Google publicado em 2026 reforça esse ponto com dados. Nele, o agente documentou pré-condições, pós-condições e comportamentos indefinidos antes de gerar testes.
Com isso, a taxa de detecção de bugs reais subiu cerca de 9,8 pontos percentuais. A comparação considerou um agente tradicional, que gerava testes direto do código.
Em outras palavras, raciocinar sobre o contrato antes do teste melhora a qualidade da suíte. O spec-driven testing vai além, porque o contrato nasce de um arquivo revisado por humanos.
O fluxo de spec-driven testing em quatro etapas
O fluxo completo tem quatro etapas, e a ordem importa. Primeiro vem a especificação, depois os artefatos de teste, em seguida a suíte que falha e, por fim, a implementação.
Etapa 1: a especificação como fonte da verdade
Tudo começa com um arquivo de especificação que descreve o que o sistema deve fazer, e não como. Contudo, para servir de base aos testes, ela precisa de um detalhe extra: um identificador estável por regra.
Identificadores como ORD-01 ou ORD-02 permitem rastrear qualquer teste até a regra que o originou. Além disso, eles transformam a especificação em algo que um script consegue ler, contar e comparar.
Etapa 2: compilar a spec em contratos e fixtures
Nesta etapa, o agente lê a especificação e gera três tipos de artefato. O primeiro é o contrato da API, normalmente um documento OpenAPI com rotas, schemas e códigos de status.
O segundo artefato são as fixtures, isto é, arquivos com os exemplos válidos e inválidos da spec. Por sua vez, o terceiro artefato é um mapa de rastreabilidade que liga cada identificador de regra aos testes correspondentes.
Chamamos essa etapa de compilação porque ela é determinística em espírito. A mesma spec sempre produz os mesmos contratos e fixtures, então qualquer diferença indica uma regra alterada.
Etapa 3: gerar a suíte que falha
Com os artefatos prontos, o agente escreve os testes de contrato e de integração. Nesse momento, nenhum endpoint existe, então todos os testes precisam falhar.
Esse detalhe funciona como um alarme.
Se um teste passa sem implementação, ele testa a coisa errada ou a spec tem um problema. Por isso, o fluxo para até que alguém resolva a dúvida.
Etapa 4: só então implementar
Somente agora o agente toca a camada de aplicação. O objetivo dele passa a ser simples e verificável: fazer a suíte derivada da spec passar sem alterar nenhum teste.
Essa restrição é a peça central do spec-driven testing.
O agente pode refatorar o código à vontade, mas não pode editar testes nem fixtures. Afinal, esses arquivos pertencem à especificação.
Exemplo prático de spec-driven testing no .NET 10
Vamos aplicar o fluxo em uma API de pedidos com .NET 10, a versão LTS atual, com Minimal APIs e xUnit. A funcionalidade é a criação de um pedido, com algumas regras de negócio simples, mas suficientes para mostrar a técnica.
A especificação com regras numeradas
O arquivo spec.md descreve a funcionalidade com regras identificadas e exemplos concretos. Repare que cada regra diz o que acontece na borda do sistema, ou seja, na requisição e na resposta HTTP.
# Feature: Create order
## Rules
- **ORD-01**: An order must contain at least one item.
Violation returns 400 with error code `order.empty`.
- **ORD-02**: An order can contain at most 50 items.
Violation returns 400 with error code `order.too_many_items`.
- **ORD-03**: Item quantity must be between 1 and 99.
Violation returns 400 with error code `item.invalid_quantity`.
- **ORD-04**: The `Idempotency-Key` header is required.
Reusing a key with a different body returns 409.
- **ORD-05**: A valid order returns 201 with the `Location` header
pointing to `/api/orders/{id}` and the computed `totalAmount`.
## Examples
| id | items | expected status | error code |
|-------|-------------------------|-----------------|-----------------------|
| EX-01 | [] | 400 | order.empty |
| EX-02 | [{qty: 0}] | 400 | item.invalid_quantity |
| EX-03 | [{qty: 2, price: 10.0}] | 201 | |
Essa spec não menciona Entity Framework, camadas ou padrões. Ela descreve apenas o comportamento observável, e é exatamente isso que permite gerar testes antes da implementação.
Fixtures derivadas da spec
Na etapa de compilação, o agente converte a tabela de exemplos em um arquivo JSON. Cada fixture carrega o identificador da regra que ela valida, o que facilita o rastreamento quando um teste falha.
[
{
"id": "EX-01",
"rule": "ORD-01",
"request": { "items": [] },
"expectedStatus": 400,
"expectedErrorCode": "order.empty"
},
{
"id": "EX-02",
"rule": "ORD-03",
"request": { "items": [ { "sku": "SKU-1", "quantity": 0, "unitPrice": 10.0 } ] },
"expectedStatus": 400,
"expectedErrorCode": "item.invalid_quantity"
},
{
"id": "EX-03",
"rule": "ORD-05",
"request": { "items": [ { "sku": "SKU-1", "quantity": 2, "unitPrice": 10.0 } ] },
"expectedStatus": 201,
"expectedTotalAmount": 20.0
}
]
O arquivo fica em specs/001-create-order/fixtures/orders.json, ao lado da spec. Dessa forma, qualquer mudança nas fixtures aparece no mesmo pull request que altera a regra.
Testes de integração com WebApplicationFactory
O ASP.NET Core oferece o WebApplicationFactory, que sobe a aplicação em memória com um TestServer. Em seguida, ele entrega um HttpClient pronto para os testes.
A documentação oficial de testes de integração no ASP.NET Core detalha a configuração completa.
O teste abaixo lê as fixtures e executa um caso para cada exemplo da spec. Por isso, adicionar um exemplo novo na spec gera automaticamente um novo caso de teste.
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
namespace Orders.Api.Tests;
public sealed class CreateOrderSpecTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client = factory.CreateClient();
public static TheoryData<OrderFixture> Fixtures()
{
// As fixtures nascem da spec, nunca do código
var json = File.ReadAllText("specs/001-create-order/fixtures/orders.json");
var items = JsonSerializer.Deserialize<List<OrderFixture>>(json, JsonSerializerOptions.Web)!;
return new TheoryData<OrderFixture>(items);
}
[Theory]
[MemberData(nameof(Fixtures))]
public async Task CreateOrder_FollowsSpec(OrderFixture fixture)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/orders")
{
Content = JsonContent.Create(fixture.Request)
};
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await _client.SendAsync(request);
// A mensagem de falha aponta para a regra da spec
Assert.True(
(int)response.StatusCode == fixture.ExpectedStatus,
$"[{fixture.Rule}/{fixture.Id}] expected {fixture.ExpectedStatus}, got {(int)response.StatusCode}");
if (fixture.ExpectedErrorCode is not null)
{
var problem = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(fixture.ExpectedErrorCode, problem.GetProperty("errorCode").GetString());
}
if (response.StatusCode == HttpStatusCode.Created)
{
Assert.NotNull(response.Headers.Location);
var order = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(fixture.ExpectedTotalAmount, order.GetProperty("totalAmount").GetDecimal());
}
}
}
public sealed record OrderFixture(
string Id,
string Rule,
JsonElement Request,
int ExpectedStatus,
string? ExpectedErrorCode,
decimal? ExpectedTotalAmount);
Repare que a mensagem de falha cita fixture.Rule. Assim, quando o pipeline quebra, o desenvolvedor sabe qual regra o código violou sem abrir o teste.
Validação do contrato OpenAPI
Os testes de integração verificam o comportamento, mas ainda falta garantir o formato da API. Para isso, o .NET 10 gera o documento OpenAPI 3.1 em tempo de build com o pacote Microsoft.Extensions.ApiDescription.Server.
Inclusive, a própria Microsoft cita testes de integração baseados em spec como um dos usos desse recurso.
<PropertyGroup>
<OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
<OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/../../artifacts/openapi</OpenApiDocumentsDirectory>
</PropertyGroup>
Com o documento gerado, um teste de contrato compara a API real com o contrato derivado da spec. O exemplo a seguir confere se cada operação e cada código de status do contrato existem na API implementada.
using System.Text.Json;
namespace Orders.Api.Tests;
public sealed class OpenApiContractTests
{
[Fact]
public void GeneratedDocument_HonorsSpecContract()
{
// Contrato derivado da spec e documento gerado no build
using var contract = JsonDocument.Parse(File.ReadAllText("specs/001-create-order/contracts/openapi.json"));
using var generated = JsonDocument.Parse(File.ReadAllText("artifacts/openapi/Orders.Api.json"));
var expectedPaths = contract.RootElement.GetProperty("paths");
var actualPaths = generated.RootElement.GetProperty("paths");
foreach (var path in expectedPaths.EnumerateObject())
{
Assert.True(actualPaths.TryGetProperty(path.Name, out var actualPath), $"Missing path {path.Name}");
foreach (var operation in path.Value.EnumerateObject())
{
Assert.True(actualPath.TryGetProperty(operation.Name, out var actualOperation),
$"Missing operation {operation.Name.ToUpper()} {path.Name}");
foreach (var status in operation.Value.GetProperty("responses").EnumerateObject())
{
Assert.True(actualOperation.GetProperty("responses").TryGetProperty(status.Name, out _),
$"Missing status {status.Name} on {operation.Name.ToUpper()} {path.Name}");
}
}
}
}
}
Esse teste é propositalmente simples, mas o princípio vale para validações mais completas, como comparar schemas campo a campo. Ferramentas como Specmatic e Schemathesis seguem a mesma lógica, porque tratam o OpenAPI como fonte da verdade.
Regressão rastreável com spec-driven testing
Um benefício pouco comentado do spec-driven testing é a regressão rastreável. Como cada teste carrega o identificador da regra, você consegue responder com precisão quais regras uma mudança quebrou.
Além disso, um script simples no pipeline verifica se toda regra da spec tem pelo menos um teste. Se alguém adicionar a regra ORD-06 e esquecer o teste, o build falha antes do merge.
#!/usr/bin/env bash
# Garante que toda regra da spec possui ao menos uma fixture ou teste
set -euo pipefail
SPEC="specs/001-create-order/spec.md"
rules=$(grep -oE 'ORD-[0-9]+' "$SPEC" | sort -u)
missing=0
for rule in $rules; do
if ! grep -rqE "\"rule\": \"$rule\"|\[$rule\]" specs/001-create-order/fixtures tests/; then
echo "Rule $rule has no test"
missing=1
fi
done
exit $missing
Esse tipo de verificação mede a cobertura pela regra, e não pela linha de código.
Na prática, a pergunta deixa de ser quantas linhas os testes executam. Agora, ela passa a ser quantas regras de negócio eles comprovam.
Spec-driven testing contra as suposições do agente
Definir o fluxo não basta, porque agentes tendem a pegar atalhos quando um teste insiste em falhar. Por isso, vale configurar algumas proteções explícitas no repositório.
A primeira proteção é tornar testes e fixtures somente leitura durante a implementação.
Você pode declarar essa regra na constituição do projeto. Depois, um arquivo CODEOWNERS exige revisão humana para qualquer mudança em specs/ e tests/.
A segunda proteção é exigir que o agente pare quando encontrar uma ambiguidade.
Por exemplo, a spec pode não dizer o que acontece com 50 itens de quantidade zero. Nesse caso, o agente registra a dúvida e volta à especificação, em vez de inventar uma resposta.
A terceira proteção é revisar os testes antes da implementação. Revisar uma suíte que falha é muito mais barato do que revisar um diff grande com código e testes misturados.
Por fim, desconfie de qualquer afrouxamento de asserção.
Trocar uma igualdade por um Contains esconde a divergência que o spec-driven testing existe para revelar. O mesmo vale para comentar um teste só para o build passar.
Vale lembrar que essas proteções dependem de uma aplicação testável.
Se você ainda acopla serviços direto nos endpoints, vale revisar a base. O artigo sobre injeção de dependência no balta.io mostra como preparar o código para os testes.
Spec-driven testing com o Spec Kit
O Spec Kit do GitHub já organiza boa parte desse fluxo. Por exemplo, o comando /speckit.plan gera o plano, a pasta contracts/, o modelo de dados e um quickstart.md.
Em seguida, o /speckit.tasks monta o tasks.md em ordem de dependência.
Quando a constituição exige TDD, os testes de contrato aparecem antes da implementação. Nesse caso, o agente precisa confirmar que eles falham.
Portanto, para aplicar spec-driven testing com o Spec Kit, você precisa de poucos ajustes. O principal é registrar três princípios na constituição: regras com identificador, fixtures vindas dos exemplos e testes imutáveis durante o /speckit.implement.
Também ajuda rodar o /speckit.analyze antes da implementação. Esse comando cruza spec, plano e tarefas, e costuma apontar regras sem tarefa de teste correspondente.
Outra prática útil é pedir que o quickstart.md cite os identificadores das regras. Assim, os cenários de validação manual e a suíte automatizada falam a mesma língua.
Spec-driven testing no pipeline de CI
O fluxo só se sustenta quando o pipeline aplica as regras de forma automática. Caso contrário, a disciplina depende da memória de cada pessoa do time, e ela falha nos dias de pressa.
Uma ordem simples funciona bem na maioria dos projetos. Primeiro, o pipeline roda o script de rastreabilidade e confirma que toda regra possui teste.
Em seguida, o build gera o documento OpenAPI e executa os testes de contrato. Depois disso, entram os testes de integração com as fixtures e, por fim, os testes de unidade.
Essa sequência privilegia o feedback mais barato. Se uma regra ficou sem teste, o pipeline falha em segundos, antes mesmo de compilar a aplicação inteira.
Também vale proteger a pasta de especificações no repositório. Uma regra de branch que exige aprovação para mudanças em specs/ impede que um agente altere a regra só para fazer o build passar.
Por último, publique o resultado por regra, e não apenas por classe de teste. Um relatório que mostra ORD-03 falhando comunica muito mais do que um nome de método.
Limitações do spec-driven testing
O spec-driven testing não elimina a necessidade de testes de unidade.
Cálculos complexos e algoritmos continuam merecendo testes próximos ao código. Enquanto isso, a suíte derivada da spec cobre a borda do sistema.
Outro cuidado envolve a qualidade da especificação.
Uma spec vaga gera testes vagos. Assim, o tempo que antes ia para os testes passa para regras claras e exemplos concretos.
Também existe o risco de drift, isto é, a spec ficar desatualizada em relação ao código.
Contudo, como a suíte nasce da spec, uma mudança de comportamento sem atualizar a especificação quebra o build. Dessa forma, o drift fica visível em vez de silencioso.
Por último, a técnica funciona melhor em APIs e serviços com contratos claros. Interfaces visuais e fluxos exploratórios ainda dependem de testes ponta a ponta e de validação humana.
Conclusão
O spec-driven testing resolve um problema que os agentes de codificação deixaram evidente. Afinal, muitos testes confirmam a implementação em vez de confirmar a regra.
Quando a especificação origina contratos, fixtures e testes, o agente trabalha contra um alvo fixo. Além disso, esse alvo passou por revisão humana antes do código.
O fluxo exige disciplina, porque a spec precisa de regras identificadas e testes que falham antes do código. Em troca, você ganha regressão rastreável e cobertura medida por regra de negócio.
Acima de tudo, você ganha uma garantia concreta de que o código faz o que a especificação promete.
Se você já usa Spec Driven Development, adotar o spec-driven testing é o próximo passo natural.
Comece por uma funcionalidade pequena e numere as regras. Depois, deixe o agente compilar a spec em testes antes de qualquer endpoint.
FAQ
Spec-driven testing substitui o TDD?
Não, porque as duas práticas se complementam. O spec-driven testing define o que validar a partir da spec, enquanto o TDD guia o design interno.
Preciso usar OpenAPI para aplicar spec-driven testing?
Não é obrigatório, mas o OpenAPI torna o contrato legível por máquinas. Sem ele, os testes de integração continuam funcionando, porém a validação de formato fica manual.
O agente pode alterar os testes gerados a partir da spec?
Durante a implementação, não. Qualquer mudança em testes ou fixtures deve passar por uma alteração na spec e por revisão humana.
Spec-driven testing funciona em projetos existentes?
Sim, desde que você escreva specs para o comportamento atual antes de gerar os testes. Nesse caso, os testes começam passando e funcionam como rede de proteção para as próximas mudanças.
Qual a diferença entre teste de contrato e teste de integração?
O teste de contrato verifica se a API respeita o formato acordado, como rotas, campos e códigos de status. Já o teste de integração verifica se as regras de negócio produzem o resultado esperado com os componentes funcionando juntos.