Depois de concluirmos esta aula bônus, nós:
- Saberemos declarar a dependência do WireMock no Gradle e no Maven.
- Saberemos como configurar o servidor HTTP do WireMock nos nossos testes de integração, simular respostas HTTP/JSON (stubbing) e verificar requisições HTTP recebidas (mocking).
- Entenderemos exatamente quando devemos usar o WireMock em vez de substituir o bean do serviço por um mock/stub em memória.
1. Obtendo as Dependências Necessárias
Antes de podermos utilizar o WireMock nos nossos testes de integração, precisamos declarar a dependência do WireMock no nosso script de build.
Se estivermos utilizando Gradle (Kotlin DSL), adicionamos a dependência ao source set de testes de integração no arquivo build.gradle.kts:
1
2
3
4
5
|
dependencies {
// WireMock standalone para JUnit Jupiter / Spring Boot 4
"integrationTestImplementation"("org.wiremock:wiremock-standalone:3.12.1")
// (Ou com.github.tomakehurst:wiremock-jre8 em projetos anteriores)
}
|
Se estivermos utilizando Maven, adicionamos a seguinte dependência ao arquivo pom.xml:
1
2
3
4
5
6
|
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.12.1</version>
<scope>test</scope>
</dependency>
|
Vamos descobrir agora como podemos configurar o servidor do WireMock nos nossos testes de integração.
2. Configurando o Servidor WireMock
Na aula anterior (Parte 1), substituímos o bean VatPercentageService por um test double quando executávamos nossos testes de integração. Fizemos isso porque essa classe obtém a porcentagem de VAT enviando uma requisição HTTP GET para uma API REST externa (http://localhost:8080/api/external/vat-percentage?countryCode=...), e queríamos eliminar a dependência externa.
Agora, com o WireMock, não substituiremos mais o bean VatPercentageService! Em vez disso, manteremos o VatPercentageService real (anotado normalmente com @Service) e iniciaremos um servidor HTTP real do WireMock ouvindo em localhost:8080 durante o teste:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package com.testwithspring.intermediate.vat
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
@Service
class VatPercentageService(
private val restTemplate: RestTemplate
) {
fun findByCountryCode(countryCode: String): VatPercentage {
return restTemplate.getForObject(
"http://localhost:8080/api/external/vat-percentage?countryCode=$countryCode",
VatPercentage::class.java
) ?: throw IllegalStateException("Resposta vazia da API externa")
}
}
|
Quando removemos todo o código específico do MockK/Mockito da nossa classe de teste de integração, o esqueleto inicial de FindVatPercentageStubTest fica assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@SpringBootTest(classes = [ExampleApplication::class])
@AutoConfigureMockMvc
@Tag("integrationTest")
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class FindVatPercentageStubTest {
companion object {
private const val COUNTRY_CODE = "FI"
private const val VAT_PERCENTAGE = 24
}
@Autowired
private lateinit var mockMvc: MockMvc
}
|
Para iniciarmos o servidor WireMock antes de cada método de teste e pará-lo após cada método de teste:
- No JUnit 4 clássico, utilizávamos a rule
@Rule public WireMockRule wireMockRule = new WireMockRule(options().bindAddress("localhost").port(8080));.
- No JUnit 6 (Jupiter), podemos usar a extensão oficial
@RegisterExtension val wireMockServer = WireMockExtension.newInstance().options(wireMockConfig().bindAddress("localhost").port(8080)).configureStaticDsl(true).build() (ou a anotação declarativa @WireMockTest(httpPort = 8080)).
Veja como fica nossa classe de teste com o servidor WireMock configurado na porta 8080:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
package com.testwithspring.intermediate.vat
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options
import com.github.tomakehurst.wiremock.junit5.WireMockExtension
import com.testwithspring.intermediate.ExampleApplication
import com.testwithspring.intermediate.Profiles
import org.hamcrest.Matchers.`is`
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
@SpringBootTest(classes = [ExampleApplication::class])
@AutoConfigureMockMvc
@Tag("integrationTest")
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class FindVatPercentageStubTest {
companion object {
private const val COUNTRY_CODE = "FI"
private const val VAT_PERCENTAGE = 24
@JvmField
@RegisterExtension
val wireMockRule: WireMockExtension = WireMockExtension.newInstance()
.options(
options()
.bindAddress("localhost")
.port(8080)
)
.configureStaticDsl(true)
.build()
}
@Autowired
private lateinit var mockMvc: MockMvc
// Métodos de teste...
}
|
Nota: Como os valores padrão do WireMock já são bindAddress("localhost") e port(8080), não precisaríamos passar essas opções explicitamente se usássemos a porta padrão 8080. Porém, configurar options().bindAddress(...).port(...) mostra exatamente como customizar o host e a porta do servidor WireMock!
A seguir, vamos descobrir como configurar a resposta HTTP retornada pelo WireMock (stubbing).
3. Configurando a Resposta HTTP Retornada (Stubbing com WireMock)
Nossos testes de integração assumem que a API REST externa retorna a alíquota 24 quando invocada com o código de país 'FI'. Podemos configurar essa resposta HTTP seguindo estes quatro passos:
- Criar um método de setup anotado com
@BeforeEach.
- Invocar o método estático
givenThat() (ou stubFor()) da classe WireMock.
- Configurar o método HTTP esperado (
WireMock.get(...)) e a URL esperada (urlEqualTo("/api/external/vat-percentage?countryCode=FI")). Repare que ao configurar a URL esperada no WireMock, não informamos o protocolo (http://), o host (localhost) nem a porta (8080) — apenas o caminho e a query string!
- Configurar o código de status HTTP retornado (
200), o cabeçalho Content-Type (application/json;charset=UTF-8) e o documento JSON no corpo da resposta (withBody(...)).
Depois de configurarmos a resposta dessa forma, nosso método @BeforeEach fica assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@BeforeEach
fun returnVatPercentage() {
givenThat(
WireMock.get(urlEqualTo("/api/external/vat-percentage?countryCode=FI"))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json;charset=UTF-8")
.withBody(
"""
{
"countryCode": "FI",
"vatPercentage": 24
}
""".trimIndent()
)
)
)
}
|
Embora isso funcione perfeitamente, esse código ainda está um pouco verboso demais. Podemos torná-lo muito mais limpo e expressivo fazendo duas melhorias:
Se a requisição HTTP esperada tiver múltiplos parâmetros de query (?a=1&b=2&c=3), usar urlEqualTo cria uma string longa e frágil (que quebra até se a ordem dos parâmetros mudar!). Por isso, é muito melhor:
- Ignorar os parâmetros de query ao configurar o caminho da URL usando
urlMatching("/api/external/vat-percentage.+") (ou urlPathEqualTo("/api/external/vat-percentage")).
- Verificar o parâmetro de query separadamente encadeando
.withQueryParam("countryCode", equalTo("FI")):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@BeforeEach
fun returnVatPercentage() {
givenThat(
WireMock.get(urlMatching("/api/external/vat-percentage.+"))
.withQueryParam("countryCode", equalTo("FI"))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json;charset=UTF-8")
.withBody(
"""
{
"countryCode": "FI",
"vatPercentage": 24
}
""".trimIndent()
)
)
)
}
|
2ª Melhoria: Simplificar a Resposta JSON com okJson()
Em vez de construirmos manualmente aResponse().withStatus(200).withHeader("Content-Type", ...).withBody(...), podemos utilizar o método fábrica okJson() da classe WireMock! Ele recebe a string JSON como argumento e já configura automaticamente o status HTTP 200 OK e o cabeçalho Content-Type: application/json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
package com.testwithspring.intermediate.vat
import com.github.tomakehurst.wiremock.client.WireMock
import com.github.tomakehurst.wiremock.client.WireMock.equalTo
import com.github.tomakehurst.wiremock.client.WireMock.givenThat
import com.github.tomakehurst.wiremock.client.WireMock.okJson
import com.github.tomakehurst.wiremock.client.WireMock.urlMatching
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options
import com.github.tomakehurst.wiremock.junit5.WireMockExtension
import com.testwithspring.intermediate.ExampleApplication
import com.testwithspring.intermediate.Profiles
import org.hamcrest.Matchers.`is`
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
@SpringBootTest(classes = [ExampleApplication::class])
@AutoConfigureMockMvc
@Tag("integrationTest")
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class FindVatPercentageStubTest {
companion object {
private const val COUNTRY_CODE = "FI"
private const val VAT_PERCENTAGE = 24
@JvmField
@RegisterExtension
val wireMockRule: WireMockExtension = WireMockExtension.newInstance()
.options(
options()
.bindAddress("localhost")
.port(8080)
)
.configureStaticDsl(true)
.build()
}
@Autowired
private lateinit var mockMvc: MockMvc
@BeforeEach
fun returnVatPercentage() {
givenThat(
WireMock.get(urlMatching("/api/external/vat-percentage.+"))
.withQueryParam("countryCode", equalTo("FI"))
.willReturn(
okJson(
"""
{
"countryCode": "FI",
"vatPercentage": 24
}
""".trimIndent()
)
)
)
}
@Test
fun `deve retornar status HTTP 200 OK`() {
findByCountryCode(COUNTRY_CODE)
.andExpect(status().isOk)
}
@Test
fun `deve retornar a porcentagem de VAT como JSON`() {
findByCountryCode(COUNTRY_CODE)
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
}
@Test
fun `deve retornar a porcentagem de VAT correta`() {
findByCountryCode(COUNTRY_CODE)
.andExpect(jsonPath("$.countryCode", `is`(COUNTRY_CODE)))
.andExpect(jsonPath("$.vatPercentage", `is`(VAT_PERCENTAGE)))
}
private fun findByCountryCode(countryCode: String): ResultActions {
return mockMvc.perform(
get("/api/vat-percentage")
.param("countryCode", countryCode)
)
}
}
|
Agora que dominamos o stubbing com WireMock, vamos ver como verificar se o servidor WireMock recebeu uma requisição HTTP específica (mocking).
Se quisermos verificar que nossa aplicação enviou uma determinada requisição HTTP para a API externa, seguimos estes dois passos:
Passo 1: Sobrescrever a Resposta Padrão do WireMock no @BeforeEach
Por padrão, o WireMock retorna erro 404 Not Found (fazendo RestTemplate lançar exceção) caso receba uma requisição HTTP para a qual nenhum stub foi configurado. Para podermos focar na verificação da requisição enviada no teste (verify), configuramos no @BeforeEach uma resposta padrão 200 OK para qualquer método e qualquer URL usando givenThat(any(anyUrl()).willReturn(ok())):
1
2
3
4
5
6
|
@BeforeEach
fun configureDefaultResponse() {
givenThat(
any(anyUrl()).willReturn(ok())
)
}
|
Primeiro, podemos verificar a URL completa usando urlEqualTo:
1
2
3
4
5
6
7
8
9
10
11
|
@Test
fun `deve buscar a porcentagem de VAT na API externa (usando urlEqualTo)`() {
findByCountryCode(COUNTRY_CODE)
verify(
1,
getRequestedFor(
urlEqualTo("/api/external/vat-percentage?countryCode=FI")
)
)
}
|
E, aplicando a mesma boa prática que aprendemos na seção anterior para evitar URLs longas com múltiplos query parameters, separamos a verificação do caminho da URL (urlMatching) da verificação do parâmetro countryCode (withQueryParam):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
package com.testwithspring.intermediate.vat
import com.github.tomakehurst.wiremock.client.WireMock.any
import com.github.tomakehurst.wiremock.client.WireMock.anyUrl
import com.github.tomakehurst.wiremock.client.WireMock.equalTo
import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor
import com.github.tomakehurst.wiremock.client.WireMock.givenThat
import com.github.tomakehurst.wiremock.client.WireMock.ok
import com.github.tomakehurst.wiremock.client.WireMock.urlMatching
import com.github.tomakehurst.wiremock.client.WireMock.verify
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options
import com.github.tomakehurst.wiremock.junit5.WireMockExtension
import com.testwithspring.intermediate.ExampleApplication
import com.testwithspring.intermediate.Profiles
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
@SpringBootTest(classes = [ExampleApplication::class])
@AutoConfigureMockMvc
@Tag("integrationTest")
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class FindVatPercentageMockTest {
companion object {
private const val COUNTRY_CODE = "FI"
@JvmField
@RegisterExtension
val wireMockRule: WireMockExtension = WireMockExtension.newInstance()
.options(
options()
.bindAddress("localhost")
.port(8080)
)
.configureStaticDsl(true)
.build()
}
@Autowired
private lateinit var mockMvc: MockMvc
@BeforeEach
fun configureDefaultResponse() {
givenThat(
any(anyUrl()).willReturn(ok())
)
}
@Test
fun `deve buscar a porcentagem de VAT na API externa com o parametro countryCode correto`() {
findByCountryCode(COUNTRY_CODE)
verify(
1,
getRequestedFor(urlMatching("/api/external/vat-percentage.+"))
.withQueryParam("countryCode", equalTo(COUNTRY_CODE))
)
}
private fun findByCountryCode(countryCode: String): ResultActions {
return mockMvc.perform(
get("/api/vat-percentage")
.param("countryCode", countryCode)
)
}
}
|
5. Quando Devemos Utilizar o WireMock?
A única coisa que determina se devemos usar o WireMock (em vez de substituir o bean do serviço por um mock/stub com MockK/Mockito) é a complexidade do código que consome a API REST externa.
Podemos tomar essa decisão guiados por estas duas regras práticas:
- Quando NÃO usar o WireMock: Se o nosso código cliente simplesmente envia uma requisição HTTP para a API REST externa e retorna a resposta exatamente como veio (um mero repasse sem lógica de transformação), não precisamos subir um servidor HTTP com WireMock — usar um stub com MockK/Mockito ou um Stub Clássico é mais simples e rápido, pois não há lógica complexa no cliente HTTP que estejamos deixando de testar.
- Quando USAR o WireMock: Se o nosso código envia requisições HTTP para a API REST externa e manipula/transforma a resposta retornada, trata códigos de erro HTTP customizados, aplica retries/timeouts ou combina dados de múltiplas fontes externas, devemos usar o WireMock! Nesses casos, o código do cliente HTTP é complexo o suficiente para exigir testes automatizados reais de serialização/deserialização JSON e protocolo HTTP, e o WireMock nos permite testar tudo isso de ponta a ponta.
Resumo
Nesta aula bônus, aprendemos três pontos essenciais:
- Podemos configurar e gerenciar o ciclo de vida do servidor HTTP do WireMock nos nossos testes usando
WireMockExtension (JUnit 6) ou WireMockRule (JUnit 4).
- Com
givenThat(get(urlMatching(...)).withQueryParam(...).willReturn(okJson(...))) e verify(1, getRequestedFor(...)), exercitamos o cliente HTTP real (RestTemplate / RestClient) e o mapeamento JSON do Jackson sem depender da rede externa.
- Devemos usar o WireMock sempre que o código que consome a API REST externa tiver lógica de negócio, transformação de payloads, agregação de múltiplas chamadas ou tratamento de erros HTTP que mereça ser testado.