Recentemente, um dos times com quem trabalho estava enfrentando dificuldades para realizar tarefas envolvendo o consumo de APIs externas (parceiros). O problema ocorria devido à indisponibilidade de um serviço que precisava ser consumido em um ambiente não produtivo para realização de um determinado teste, o que estava causando impactos negativos no processo de desenvolvimento de software, inclusive na produtividade do time.
Diante desse cenário, surge o tema que quero abordar hoje: o Wiremock, uma poderosa ferramenta que permite simular APIs e realizar muitas tarefas que facilitam a vida de quem precisa lidar com integrações entre APIs, testes e outras atividades relacionadas. Vamos lá!
O que iremos ver aqui:
- O que é um Mock?
- Motivação para o uso de uma ferramenta para simular APIs (mocks)
- Quando utilizar?
- Resumo sobre Wiremock
- Principais funcionalidades do Wiremock
- Maneiras de executar o Wiremock
- Estrutura de um projeto Wiremock standalone
- Funcionamento básico
- Executando um projeto Wiremock
- Exemplos de configurações de mocks
- Conclusão
1. O que é um mock?
Mock é uma técnica utilizada principalmente em testes de software para simular o comportamento de componentes reais. Ao criar um mock, estamos basicamente criando uma “imitação” de um componente específico, de forma que ele responda de maneira predefinida, sem executar a lógica real por trás dele.
2. Motivação para o uso de uma ferramenta para simular APIs (mocks):
Com a modernização da forma como desenvolvemos software, especialmente em uma arquitetura de microsserviços, onde cada componente possui suas próprias responsabilidades, é comum a interação entre APIs. Essa interação pode ser desde a implementação de uma nova API ou até o consumo de outra API, seja interna ou externa.
Algumas necessidades comuns no desenvolvimento de software incluem:
- Evitar requisições a APIs de terceiros em ambientes não produtivos (sandbox, homologação), reduzindo custos desnecessários e impactos de indisponibilidade;
- Desenvolver com base na documentação da API, sem um ambiente de sandbox disponível para testes;
- Testar diferentes comportamentos da API, como tipos de resposta, latência e falhas;
- Reduzir a dependência de um time para finalizar a implementação de uma API, permitindo que outros times avancem;
- Realizar testes de carga/performance em serviços com várias integrações, sem gerar requisições desnecessárias ou efeitos colaterais em outros microsserviços devido à alta volumetria de requisições.
Esses exemplos, entre outras situações, podem ser solucionados com um simulador de APIs, permitindo "mockar" a API necessária.
3. Quando utilizar?
No contexto do desenvolvimento de software e na interação entre APIs, é comum precisarmos:
- Simular e testar integrações com APIs ainda não disponíveis;
- Evitar consultas excessivas às APIs de origem em ambientes não produtivos;
- Realizar testes e validações de integrações em tempo de desenvolvimento;
- Executar testes automatizados de integração;
- Validar diferentes cenários de resposta.
Essas são situações em que o uso de um simulador de APIs pode ser a solução.
4. Resumo sobre o Wiremock
O Wiremock é uma ferramenta open-source flexível para simular serviços HTTP, permitindo criar mocks e servidores de mocks para APIs ou serviços web. Dessa forma, podemos replicar respostas e comportamentos conforme necessário, sem acessar o serviço real, como se tivéssemos uma cópia da API que precisamos consumir, com total liberdade para simular comportamentos. Isso assegura um ambiente de integração estável entre APIs, sem dependência de terceiros.
5. Principais funcionalidades do Wiremock
- Esboço e simulação de respostas de APIs por compatibilidade de padrões: Cria simulações dinâmicas com base nos padrões das requisições recebidas.
- Prioridade de respostas para requisições semelhantes: Define a resposta com base na prioridade das chamadas.
- Simulação de atrasos e falhas nas respostas: Simula latência e falhas nas respostas para testar diferentes cenários.
- Execução standalone ou embutida na aplicação: Funciona como um serviço autônomo ou embutido na aplicação.
- Proxy condicional baseado em requisições: Direciona requisições para outros serviços com base em condições específicas.
- Gravação e reprodução de interações: Registra interações com o serviço e as reproduz como mocks.
- API para administração de funcionalidades: Facilita o gerenciamento programático de funcionalidades, suportando a criação de scripts e automação de processos.
6. Maneiras de executar o Wiremock
Existem diferentes formas de executar o Wiremock. Podemos incluir a dependência do Wiremock em nosso projeto e implementar as funcionalidades de forma embutida na própria aplicação ou, alternativamente, executar um servidor autônomo que roda como um projeto independente, contendo os mapeamentos da API que será simulada.
Para nossos exemplos, utilizaremos a abordagem onde será criado um servidor standalone.
7. Estrutura de um projeto Wiremock standalone
Em um projeto Wiremock standalone, a estrutura básica é organizada da seguinte forma:
- mappings/: pasta onde são colocados os arquivos JSON que definem as requisições e respostas simuladas. Cada arquivo representa uma simulação de requisição-resposta (mock).
- files/: pasta onde são armazenados arquivos de resposta mais complexos. Em vez de definir o corpo da resposta diretamente no JSON de mapeamento, é possível referenciar arquivos armazenados na pasta
files/
usando o atributobodyFileName
.
8. Funcionamento básico
a) Mapeamento de Requisições: Defina as características da requisição (como método, URL e headers) no arquivo JSON dentro da pasta mappings/
.
b) Configuração de Respostas: Dentro do mesmo arquivo, configure a resposta desejada para cada requisição, como o status HTTP, o corpo e os headers.
c) Execução: Quando o Wiremock recebe uma requisição que corresponde a uma configuração em mappings/
, ele responde conforme especificado.
Essa estrutura permite organizar as simulações e realizar testes de forma isolada e independente, considerando os diversos possíveis cenários existentes.
9. Executando um projeto Wiremock
Para os exemplos aqui utilizados iremos utilizar um projeto já existente, conforme link do repositório abaixo:
Para rodar o Wiremock no modo standalone, use o seguinte comando:
java -jar <name_jar>.jar --global-response-templating --no-request-journal --port <port>
Exemplo:
java -jar wiremock-standalone-3.9.1.jar --global-response-templating --no-request-journal --port 9090 --verbose
Resultado da execução:
Pronto, temos nosso servidor mock standalone sendo executado e pronto para receber as requisições.
10. Exemplos de configurações de mocks
a) Simulação de requisição GET — Sem utilizar arquivo de resposta:
mappings/ -> get_data_basic.json
{
"id" : "1768945d-c8f4-45f3-ad69-f80237ae8fa3",
"name" : "get-resource-example1-return-200",
"request" : {
"urlPattern" : "/v1/tests",
"method" : "GET"
},
"response" : {
"status" : 200,
"body": "{\"id\": \"1\", \"result\": \"data\" }",
"headers" : {
"Content-Type" : "application/json;charset=UTF-8"
}
},
"uuid" : "1768945d-c8f4-45f3-ad69-f80237ae8fa3",
"persistent" : true,
"priority": 1
}
Note que no exemplo acima, o response está sendo montado diretamente no atributo body
do objeto response
, quando temos um response simples, isso é o suficiente, mas caso o response seja complexo, precisamos criar um arquivo para tal finalidade.
b) Simulação de requisição GET — Utilizando arquivo de resposta (pasta files/):
mappings/ -> get_data_file.json
{
"id" : "357d4984-eb5b-49cb-9f02-f85b65b9b6d3",
"name" : "get-resource-example2-return-200",
"request" : {
"urlPattern" : "/v1/tests",
"method" : "GET"
},
"response" : {
"status" : 200,
"bodyFileName" : "get_data_file.json",
"headers" : {
"Content-Type" : "application/json;charset=UTF-8"
}
},
"uuid" : "357d4984-eb5b-49cb-9f02-f85b65b9b6d3",
"persistent" : true,
"priority": 2
}
files/ -> get_data_file.json
{
"id": "2",
"result": "data returned file"
}
No exemplo acima, temos basicamente o mesmo mapeamento do exemplo anterior, porém, no atributo bodyFileName
do objeto response
, estamos fazendo referência a um arquivo separado que contém a definição do conteúdo do response, localizado na pasta files/
. Com isso podemos definir responses mais complexos conforme a necessidade.
c) Simulação de requisição GET — Simulando erro 404 e atraso na resposta:
mappings/ -> get_data_error_404_with_response_delay.json
{
"id" : "978d8cf8-f17f-4990-8acd-19911d3f4782",
"name" : "get-resource-error-404-response-delay",
"request" : {
"urlPattern" : "/v1/tests/1",
"method" : "GET"
},
"response" : {
"status" : 404,
"fixedDelayMilliseconds": "2000",
"bodyFileName" : "get_data_error_404.json",
"headers" : {
"Content-Type" : "application/json;charset=UTF-8"
}
},
"uuid" : "978d8cf8-f17f-4990-8acd-19911d3f4782",
"persistent" : true,
"priority": 1
}
files/ -> get_data_error_404.json
{
"code": "NOT_FOUND",
"message": "Not Found",
"details": [
"Not Found Error"
]
}
No exemplo acima temos um cenário onde precisamos simular o atraso de resposta de uma API, seja para validar alguma regra de timeout, ou algum desvio de fluxo. No mapeamento definido em mappings/
, podemos utilizar o atributo fixedDelayMilliseconds
no objeto de response
, que irá realizar essa funcionalidade de atraso na resposta.
d) Simulação de requisição POST baseado em padrões — Simulando resposta dinâmica:
mappings/ -> post_data_with_pattern.json
{
"name": "post_data_with_pattern_200",
"request": {
"urlPathPattern": "/v1/product/([0-9]{5})/validate",
"method": "POST",
"bodyPatterns": [ {
"contains" : "\"PROCESSING\""
}]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"bodyFileName": "post_data_with_pattern_200.json",
"transformers": [
"response-template"
]
},
"persistent": true,
"priority": 1
}
files/ -> post_data_with_pattern_200.json
{
"id": "{{randomValue length=7 type='NUMERIC'}}",
"sku": "{{request.path.[2]}}",
"name": "{{jsonPath request.body '$.name'}}",
"status": "{{jsonPath request.body '$.status'}}",
"result": "ALLOWED"
}
cURL:
curl --location 'http://localhost:9090/v1/product/12345/validate' \
--header 'Content-Type: application/json' \
--data '{
"name": "Product 1",
"status": "PROCESSING"
}'
No exemplo acima, podemos notar que o Wiremock é uma ferramenta poderosa que vai além de simulações relativamente simples, para cenários mais complexos, podemos utilizar o mapeamento baseado em padrões de correspondência de URL e corpo da requisição, além de mapear uma resposta mais dinâmica, como por exemplo, definir um ID randômico, extrair dados da requisição e utilizar como parte para a resposta, entre outras opções.
Para auxiliar no entendimento do exemplo acima, vamos detalhar abaixo o funcionamento de alguns atributos utilizados:
mappings/ -> post_data_with_pattern.json
"urlPathPattern": "/v1/product/([0-9]{5})/validate"
Esta expressão regular define que o endpoint deve ter o caminho /v1/product/
, seguido de exatamente cinco dígitos numéricos, e terminando com /validate
. Exemplo de URL válida: /v1/product/12345/validate
.
"bodyPatterns": [{
"contains": "\"PROCESSING\""
}]
Aqui, é definido um padrão para o corpo da requisição. A requisição deve conter a palavra "PROCESSING"
para corresponder a este mapeamento. Isso permite verificar se o valor específico está no corpo da requisição enviada.
"transformers": [
"response-template"
]
O atributo transformador response-template
, permite aplicar templates na resposta usando dados dinâmicos (placeholders) para personalizar o conteúdo da resposta com base na requisição enviada.
files/ -> post_data_with_pattern_200.json
"id": "{{randomValue length=7 type='NUMERIC'}}"
Este atributo gera um valor numérico randômico com 7 dígitos. O randomValue
é uma função do Wiremock que permite criar dados dinâmicos na resposta. Aqui, ele gera um id
único cada vez que a resposta é retornada.
"sku": "{{request.path.[2]}}"
Este atributo extrai uma parte específica do caminho da URL da requisição. O request.path.[2]
corresponde ao terceiro segmento na URL (no caso /v1/product/12345/validate
, o valor seria 12345
). Este valor é atribuído ao campo sku
.
"status": "{{jsonPath request.body '$.status'}}"
Utiliza jsonPath
para capturar o valor de status
do corpo da requisição. Se o campo status
corresponder a "PROCESSING"
, então "PROCESSING"
será o valor preenchido neste campo de resposta.
11. Conclusão
Neste artigo podemos ter uma visão resumida sobre o Wiremock e algumas de suas funcionalidades, trata-se de uma ferramenta muito útil e com diversas casos de uso, auxiliando em diversas etapas dentro do processo de desenvolvimento de software, podendo ser utilizando localmente pelo desenvolvedor, ou até mesmo implantando em um ambiente para suportar testes de integração, testes de perfomance e demais necessidades.
Além do mais, o Wiremock possui diversas opções de distribuições, permitindo executá-lo de diferentes maneiras, além de suportar as principais stacks de tecnologia, por meio de suporte fornecido pela comunidade Wiremock e colaboradores externos.
Bom, espero que esse artigo tenha sido útil de alguma forma para você, até o próximo!
Recursos:
Top comments (2)
Your blog is always so informative! I find your analysis of API challenges particularly helpful. While working to improve my testing process, I discovered EchoAPI, and its API mocking capabilities have made it much easier to create test scenarios without waiting on a live backend.
Thank you, Philip!
I don't know EchoAPI yet, but I'll understand better how it works! Thank you for your collaboration!