SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

Mocking e testes de API em VPS próprio

Veja como combinar WireMock e Hurl em um VPS: stubs versionados no Git, testes no CI e relatórios preservados mesmo após reconstruir o servidor.

Dois trabalhos que partilham um repositório

Criar mocks de API e testar APIs localmente são dois trabalhos diferentes, e tratá-los como um só faz perder uma semana. Um servidor de mocks substitui uma dependência que não pode ser chamada a partir do CI: um fornecedor de pagamentos, uma API de um parceiro, um upstream com limite de taxa ou um serviço que outra equipa ainda não disponibilizou. Um executor de testes de API chama os seus próprios endpoints numa ordem fixa e valida as respostas, transportando valores de uma resposta para o pedido seguinte.

Os dois não se sobrepõem. Um servidor de mocks nunca indica aprovação ou falha. Um executor de testes não determina o que um fornecedor de pagamentos devolve quando um cartão é recusado. A maioria das equipas que já aluga um servidor acaba por executar uma instância de cada um, iniciadas pelo mesmo ficheiro Docker Compose e revistas no mesmo pull request.

Por que hospedar internamente o mocking e os testes de API?

Os seus fixtures contêm dados com o formato dos dados de produção. O corpo de uma requisição num teste de API é um registo real de cliente com o nome alterado, ou com o nome inalterado porque ninguém o verificou. Os stubs gravados são ainda piores: a gravação através de um proxy armazena o que o upstream devolveu efetivamente. Por isso, um diretório de stubs criado por gravação contém tokens ativos e endereços de e-mail de clientes até alguém ler todos os ficheiros. Num serviço alojado, esses dados tornam-se um incidente da responsabilidade de terceiros e uma divulgação de dados sua.

A segunda razão é a acessibilidade. Um serviço associado a um endereço privado não pode ser acedido a partir de um runner alojado, pelo que o teste nem sequer pode ser executado. Todas as alternativas têm um custo. Publicar a API na Internet para a testar elimina a razão pela qual ela era privada. Um túnel ou uma cópia pública de staging é outro sistema para manter, e uma cópia de staging fica diferente da produção entre releases. Um runner na mesma rede privada chama o serviço diretamente e não precisa de nada disso. Esse é o argumento prático a favor de um runner self-hosted do GitHub Actions.

Qual servidor mock auto-hospedado deve executar?

Cada um destes servidores é executado como um contentor num servidor que controla. O aspeto importante é saber qual considera ser a fonte de verdade, porque isso determina se recriar o contentor não tem custo ou se consome uma tarde inteira.

  • WireMock mantém cada stub como um ficheiro JSON num diretório mappings/, com corpos de resposta grandes em __files/. A imagem é wiremock/wiremock, o diretório raiz dentro do contentor é /home/wiremock e também funciona como proxy de gravação. Os ficheiros em disco significam que o mock pode ficar no git como qualquer outro código.
  • Mockoon CLI mantém uma API mock completa num único ficheiro de dados JSON. Instale-o com npm install -g @mockoon/cli e inicie-o com mockoon-cli start --data ./data-file.json, ou execute a imagem mockoon/cli com esse ficheiro montado por bind. A aplicação de desktop edita o mesmo ficheiro, por isso é possível criar a configuração numa interface gráfica e fazer commit do resultado sem incompatibilidades.
  • MockServer é executado a partir da imagem mockserver/mockserver e escuta na porta 1080. As expectativas são enviadas através da sua própria API REST, o que é útil a partir do código de testes, mas arriscado numa implementação: uma expectativa criada por uma chamada HTTP desaparece quando o contentor reinicia. Use o ficheiro de inicialização JSON para os stubs que devem ser permanentes.
  • Prism cria o mock a partir do documento OpenAPI, em vez de usar ficheiros de stubs separados. Instale-o com npm install -g @stoplight/prism-cli e execute-o com prism mock openapi.yaml. Dentro de um contentor, adicione -h 0.0.0.0, porque o Prism faz bind a localhost por predefinição e, caso contrário, não fica acessível a partir do exterior do contentor.
  • Microcks é a opção mais completa: fornece uma interface web que importa documentos OpenAPI e coleções do Postman, depois disponibiliza-os como mocks e executa testes de contrato. Uma instalação completa precisa de MongoDB e Keycloak, além de Kafka para as funcionalidades assíncronas. A imagem microcks-uber tudo-em-um inclui um MongoDB em memória, que o projeto documenta como adequado para uso efémero. Por isso, trate tudo o que for criado nessa interface como descartável e mantenha os artefactos de origem no git.

Qual executor de testes de API self-hosted deve utilizar?

A tarefa é uma sequência: autenticar, criar um pedido, lê-lo novamente e verificar se o estado mudou. Isto exige capturar um valor de uma resposta e utilizá-lo no pedido seguinte. Uma ferramenta que não consegue manter o estado entre chamadas é uma verificação de disponibilidade, não um teste de API.

  • Hurl executa ficheiros de texto simples com pedidos HTTP a partir de um único binário. Uma secção [Captures] extrai valores de uma resposta, uma secção [Asserts] verifica-os e --test transforma o processo num executor de testes com um resumo e um código de saída. A versão 8.0.1 é a atual em agosto de 2026.
  • Bruno CLI executa uma pasta de ficheiros .bru. Instale com npm install -g @usebruno/cli e depois execute bru run folder --env Local --reporter-junit results.xml. O formato da coleção é constituído, por conceção, por ficheiros de texto num diretório, pelo que as diferenças são fáceis de ler durante a revisão.
  • Newman executa coleções do Postman fora do Postman: npm install -g newman e depois newman run collection.json -r cli,junit --reporter-junit-export results.xml. O problema está no formato. A coleção é um único bloco JSON exportado. A edição é feita no Postman e o ficheiro no git é uma cópia que fica desatualizada.
  • Schemathesis é um tipo diferente de verificação. Lê um esquema OpenAPI e gera casos que tentam produzir respostas que o esquema indica serem impossíveis: uvx schemathesis run https://your.api/openapi.json. Encontra falhas e violações do contrato, mas não conhece as regras de negócio. Por isso, deve ser utilizado juntamente com uma suite de testes com scripts, e não como substituto.
  • Hoppscotch self-hosted é a opção com interface web e requer uma instância do Postgres. Considere essa limitação antes de o instalar: as coleções ficam numa base de dados, não no seu repositório.

Uma opção a evitar. O Step CI continua a aparecer em comparativos de ferramentas e o seu formato de workflow em YAML é legível, mas o repositório recebeu o último commit em agosto de 2024. Um programa que fica entre o seu CI e a sua API é um local inadequado para código sem manutenção.

Coloque o mock server atrás da firewall

A configuração abaixo executa o WireMock como substituto de um fornecedor de pagamentos. Se o formato do ficheiro Compose for novo para si, Docker Compose num VPS aborda os comandos de ciclo de vida assumidos nesta secção.

services:
  mock-payments:
    image: wiremock/wiremock:3.13.2
    command: ["--verbose"]
    volumes:
      - ./mocks/payments:/home/wiremock
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

O prefixo 127.0.0.1: na porta é a parte importante. Um 8080:8080 simples publica o mock em todas as interfaces, incluindo o seu IP público, e este continua acessível mesmo com o ufw a bloquear essa porta, porque o Docker escreve as suas próprias regras na cadeia DOCKER do iptables, que são avaliadas antes das regras INPUT do ufw. Faça o bind ao endereço de loopback ou ao endereço de uma interface privada. Assim, o kernel nunca aceita a ligação a partir do exterior.

O serviço que está a testar aponta então para o mock. Quando o serviço é executado no mesmo projeto Compose, o URL base do mock é http://mock-payments:8080, porque o Compose resolve os nomes dos serviços na sua própria rede. Quando o serviço é executado no host, é http://127.0.0.1:8080. Defina esse valor através de uma variável de ambiente, nunca no código. Caso contrário, o URL de teste pode ser incluído no software de produção.

Os stubs ficam em ./mocks/payments/mappings/, com um ficheiro JSON por stub.

{
  "request": {
    "method": "POST",
    "urlPath": "/v1/charges",
    "bodyPatterns": [{ "matchesJsonPath": "$.amount" }]
  },
  "response": {
    "status": 201,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": { "id": "ch_test_001", "status": "succeeded", "amount": 4200 }
  }
}

Inicie-o e verifique depois o que foi realmente carregado.

docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings

--wait bloqueia até o contentor indicar um estado saudável. Isto funciona porque a imagem WireMock inclui um HEALTHCHECK para o endpoint /__admin/health. A chamada mappings lista todos os stubs lidos pelo servidor. Se um stub que criou não aparecer nessa lista, nunca foi carregado: confirme que o ficheiro está em mappings/ e não na raiz montada, e confirme que o JSON é válido.

Quando chega um pedido e nenhum stub corresponde, o WireMock responde com 404 e um corpo que começa por Request was not matched, seguido de uma diferença em relação ao stub mais próximo que tem. Leia essa diferença antes de alterar qualquer coisa, porque ela identifica o campo exato que é diferente. Normalmente trata-se de um caminho com /v1/charge, enquanto o stub indica /v1/charges.

Escreva o teste como uma sequência com estado transferido entre chamadas

Os ficheiros Hurl são texto simples. Instale o pacote deb a partir dos releases do projeto.

VERSION=8.0.1
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VERSION/hurl_${VERSION}_amd64.deb
sudo apt update && sudo apt install ./hurl_${VERSION}_amd64.deb

Uma suite que testa a sua própria API contra o mock está em tests/checkout.hurl.

POST {{base_url}}/orders
Content-Type: application/json
{
  "sku": "ssd-1tb",
  "amount": 4200
}
HTTP 201
[Captures]
order_id: jsonpath "$['id']"

GET {{base_url}}/orders/{{order_id}}
HTTP 200
[Asserts]
jsonpath "$.status" == "paid"
jsonpath "$.charge_id" == "ch_test_001"

O bloco [Captures] é o que transforma isto num teste de API, em vez de duas requisições não relacionadas. order_id é lido da primeira resposta e interpolado no URL da segunda. A asserção em charge_id é o objetivo de todo o exercício: prova que o seu serviço chamou o fornecedor de pagamentos e armazenou o que recebeu. O valor usado na comparação é o que escreveu no stub do WireMock. Um único ficheiro cobre agora as duas partes do fluxo.

hurl --test --variable base_url=http://127.0.0.1:3000 \
  --report-junit reports/junit.xml \
  --report-json reports/json \
  tests/

Uma execução bem-sucedida imprime uma linha por ficheiro e um resumo.

tests/checkout.hurl: Success (2 request(s) in 61 ms)
Executed files:    1
Executed requests: 2 (30.1/s)
Succeeded files:   1 (100.0%)
Failed files:      0 (0.0%)
Duration:          64 ms

Uma falha imprime error: Assert failure com o ficheiro e o número da linha, depois o valor obtido e o valor esperado. Em seguida, hurl termina com um código diferente de zero, para que a CI seja interrompida. Se status ler pending quando esperava paid, o seu serviço não processou a resposta do mock. O próximo elemento a consultar é o diário de requisições do WireMock em /__admin/requests, que mostra se a chamada chegou ao mock.

Acione a suíte a partir do seu próprio runner de CI

Com um runner registado no mesmo host, o workflow é curto. O runner é um processo comum no host, portanto docker e hurl precisam estar instalados nesse host. Nada é herdado de uma imagem alojada.

name: api-tests
on: [push]
jobs:
  hurl:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Start the mock
        run: docker compose up -d --wait mock-payments
      - name: Run the suite
        run: hurl --test --variable base_url=http://127.0.0.1:3000 --report-junit reports/junit.xml tests/
      - name: Archive the reports
        if: always()
        run: install -d /srv/api-tests/reports/$GITHUB_SHA && cp -r reports/. /srv/api-tests/reports/$GITHUB_SHA/
      - name: Stop the mock
        if: always()
        run: docker compose down

if: always() na etapa de arquivamento é importante. Sem ele, uma execução de testes com falha ignora a cópia e o relatório que queria consultar é perdido. A cópia também precisa ser colocada fora do workspace, porque o runner limpa o workspace antes do próximo job e os relatórios são eliminados com ele.

Preserve os resultados, não apenas a última execução

Um ficheiro XML JUnit por commit responde a uma pergunta: passou? Não responde a partir de quando um endpoint começou a ficar mais lento, porque nada lê esses ficheiros depois de deixar de os abrir. Para obter uma tendência, acrescente uma linha por execução a uma pequena base de dados no mesmo servidor. Basta uma tabela com o SHA do commit, o nome do ficheiro, o número de testes aprovados, o número de testes falhados e a duração. SQLite em produção numa VPS é uma opção adequada: um único ficheiro, sem processo de servidor, e todo o histórico fica incluído na cópia de segurança que já faz. Analise a saída --report-json do Hurl em vez do XML JUnit, porque é o formato legível por máquina entre os dois.

O que tem de sobreviver à reconstrução de um contentor

As definições de mocks e as suites de testes são código-fonte. Devem estar num repositório junto do serviço que descrevem e ser alteradas no mesmo pull request que altera um endpoint. Um stub editado numa interface Web, ou uma expectativa enviada para o MockServer através da sua API REST em tempo de execução, existe apenas na memória desse contentor ou na base de dados dessa ferramenta. Execute docker compose down e ele desaparece. Ninguém dá por isso até um teste começar a passar pelo motivo errado. Se os seus repositórios também estiverem no seu próprio hardware, um servidor git autoalojado mantém os fixtures e o serviço dentro do mesmo limite de confiança.

Seguem as regras práticas. Fixe as tags das imagens, porque latest pode alterar a forma como o seu mock compara os pedidos sem qualquer alteração no repositório, e é muito difícil relacionar essa falha com a causa. Monte os diretórios de stubs apenas para leitura quando a ferramenta não precisar de escrever neles. Nunca coloque os stubs de um mock num volume Docker nomeado, porque o volume passa a ser a fonte de verdade e a cópia no git fica silenciosamente incorreta.

Há ainda outra regra que apanha muita gente. Se criar stubs gravando tráfego real através de um proxy, leia todos os ficheiros gerados antes de os colocar no commit. Uma gravação contém exatamente o que o upstream devolveu, incluindo bearer tokens e endereços de email de clientes. Colocá-la no commit torna esses dados permanentes no repositório, porque o git mantém o conteúdo eliminado no histórico.

FAQ

Qual é a diferença entre um servidor mock de API e um executor de testes de API?

Um servidor mock responde a pedidos. Ele substitui uma dependência que não pode ser chamada a partir do CI e nunca indica aprovação ou falha. Um executor de testes de API envia pedidos para o seu próprio serviço, valida as respostas, transporta valores de uma chamada para a seguinte e termina com código diferente de zero quando uma validação falha. Eles resolvem problemas diferentes. Uma configuração típica executa ambos ao mesmo tempo: o executor chama o seu serviço, enquanto o seu serviço chama o mock.

Posso testar uma API interna a partir de um executor de CI alojado?

Não sem expor a API. Um executor alojado fica fora da sua rede, por isso não consegue alcançar um serviço associado a um endereço privado. As opções são publicar a API, executar um túnel ou manter uma cópia pública de staging. Cada opção acrescenta um sistema que pode falhar ou expor dados. Um executor na mesma rede privada chama o serviço diretamente. Essa é a principal razão prática para as equipas alojarem este trabalho na própria infraestrutura.

Onde devem ficar os stubs dos mocks e as suites de testes de API?

No git, junto do serviço que descrevem. As ferramentas que armazenam definições como ficheiros, como o diretório mappings/ do WireMock, o ficheiro de dados do Mockoon, os ficheiros Hurl e a pasta .bru do Bruno, permitem revisão de código e uma reconstrução do contentor sem custo adicional. As ferramentas que armazenam definições numa base de dados ou numa interface web precisam de uma estratégia de cópia de segurança e de uma etapa de exportação. A exportação é precisamente a parte que as pessoas esquecem até o contentor já ter desaparecido.

Por que motivo o meu mock devolve 404 quando o stub parece correto?

O WireMock disponibiliza um stub apenas quando existe uma correspondência exata. Um pedido sem correspondência recebe 404, com um corpo que começa por Request was not matched, seguido de uma diferença em relação ao stub mais próximo. Essa diferença identifica o campo que não coincide. As causas comuns são uma barra final no caminho, um cabeçalho Content-Type exigido pelo stub que o cliente não enviou, urlPath utilizado quando o stub precisa de urlPathPattern para um segmento variável e um comparador de corpo que não corresponde ao payload. Verifique primeiro /__admin/requests para confirmar que o pedido chegou efetivamente ao mock.

Ainda preciso de mocks se tiver um ambiente de staging?

Sim, por dois motivos. Uma cópia de staging de um serviço upstream que não controla pode ficar indisponível e continuar a aplicar limites de pedidos. Nesse caso, a suite falha por motivos sem relação com o seu código. Além disso, essa cópia não consegue produzir as respostas que mais precisa de testar, como um cartão recusado ou um timeout do gateway. Um mock devolve essas respostas a pedido, à velocidade da rede local. Isso reduz uma suite que demora minutos contra um sandbox a uma suite que demora segundos. Mantenha o staging para a verificação final antes do release e utilize mocks no CI.