WireMock Testing
WireMock is a great library to mock APIs in your tests and supports Junit5 with two modes:
- Declarative with @WireMockTest
- Programmatic with WireMockExtension
And WireMock also has an official Docker image!
But "talk is cheap, show me the code ..."
Ok so let's implement first the scenario with @WireMockTest:
And later the one with WireMock's official Docker image:
BarClient
BarClient interface
interface BarClient {
fun call(name: String): String
}
BarKtorClient test
I will use a Ktor client for no other reason that I need an Http client and this seems interesting, as we are using Kotlin.
So a simple @WireMockTest for the BarKtorClient looks like:
@WireMockTest
class BarKtorClientShould {
private val name = "Sue"
@Test
fun `call bar api`(wm: WireMockRuntimeInfo) {
stubFor(
get(urlPathMatching("/bar/$name"))
.willReturn(ok().withBody("Hello $name I am Bar!"))
)
assertThat(
BarKtorClient(wm.httpBaseUrl).call(name)
).isEqualTo("Hello $name I am Bar!")
}
@Test
fun `handle bar api server error`(wm: WireMockRuntimeInfo) {
stubFor(
get(urlPathMatching("/bar/.+"))
.willReturn(serverError())
)
assertThat(BarKtorClient(wm.httpBaseUrl).call(name))
.startsWith("Bar api error: Server error")
}
}
BarKtorClient implementation
In order to make the test pass
class BarKtorClient(private val url: String) : BarClient {
private val client = HttpClient(CIO)
override fun call(name: String): String = runBlocking {
try {
client.get("$url/bar/$name")
} catch (e: Exception) {
"Bar api error: ${e.message}"
}
}
}
FooClient
FooClient interface
interface FooClient {
fun call(name: String): String
}
FooKtorClient test
For this test I want to use WireMock's response templating feature, so I will register a WireMockExtension instead of using @WireMockTest:
@TestInstance(PER_CLASS)
class FooKtorClientShould {
private val name = "Joe"
@RegisterExtension
val wm: WireMockExtension = WireMockExtension.newInstance()
.options(wireMockConfig()
.extensions(ResponseTemplateTransformer(true))
)
.configureStaticDsl(true)
.build()
@Test
fun `call foo api`() {
stubFor(
get(urlPathEqualTo("/foo"))
.withQueryParam("name", matching(".+"))
.willReturn(ok().withBody("Hello {{request.query.name}} I am Foo!"))
)
assertThat(FooKtorClient(wm.baseUrl()).call(name))
.isEqualTo("Hello $name I am Foo!")
}
@Test
fun `handle foo api server error`() {
stubFor(
get(urlPathEqualTo("/foo"))
.willReturn(WireMock.serverError())
)
assertThat(FooKtorClient(wm.baseUrl()).call(name))
.startsWith("Foo api error: Server error")
}
}
Note that:
- Instead of having a fixed response, with WireMock's response templating we can insert in the response values from the request. In this case the query parameter
name
. @TestInstance(PER_CLASS)
makes JUnit5 create a single instance of FooKtorClientShould to be used by both tests so the WireMockExtension is registered only once. By default JUnit5 would create one instance for each test (see Test Instance Lifecycle).configureStaticDsl(true)
makes it possible to use the static DSL, that is usingstubFor(...)
staticly instead ofwm.stubFor(...)
.
FooKtorClient implementation
Same as before in order to make the test pass
class FooKtorClient(private val url: String) : FooClient {
private val client = HttpClient(CIO)
override fun call(name: String): String = runBlocking {
try {
client.get("$url/foo") {
parameter("name", name)
}
} catch (e: Exception) {
"Foo api error: ${e.message}"
}
}
}
AppUseCase
Now we have to implement AppUseCase, which will use a FooClient to call the Foo API and then a BarClient to call the Bar API.
As it is not WireMock related because we can test first the implementation just using MockK JUnit5 extension we can skip the details and you can review the source code of AppUseCaseShould and AppUseCase.
App
App implementation
Let me introduce first the App implementation, as I will present later two different types of WireMock tests:
class App(
private val name: String,
private val fooApiUrl: String,
private val barApiUrl: String
) {
fun execute() = AppUseCase().execute(
name,
FooKtorClient(fooApiUrl),
BarKtorClient(barApiUrl)
)
}
App test with @WireMockTest
Since in this example Foo API and Bar API do not have conflicting endpoints, we can use one @WireMockTest to mock both APIs:
@WireMockTest
class AppShouldWithOneWireMockTest {
private val name = "Ada"
@Test
fun `call foo and bar`(wm: WireMockRuntimeInfo) {
stubFor(
get(urlPathEqualTo("/foo"))
.withQueryParam("name", equalTo(name))
.willReturn(ok().withBody("Hello $name I am Foo!"))
)
stubFor(
get(urlPathMatching("/bar/$name"))
.willReturn(ok().withBody("Hello $name I am Bar!"))
)
val app = App(name, wm.httpBaseUrl, wm.httpBaseUrl)
assertThat(app.execute()).isEqualTo(
"""
Hi! I am $name
I called Foo and its response is Hello $name I am Foo!
I called Bar and its response is Hello $name I am Bar!
Bye!
""".trimIndent()
)
}
}
App test with WireMockExtension
But imagine a real scenario where Foo API and Bar API do have conflicting endpoints, or you just want to mock them separatedly for any reason. In this case you can register two WireMockExtensions instead of using @WireMockTest:
@TestInstance(PER_CLASS)
class AppShouldWithTwoWireMockExtensions {
private val name = "Leo"
@RegisterExtension
val wireMockFoo: WireMockExtension = newInstance().build()
@RegisterExtension
val wireMockBar: WireMockExtension = newInstance().build()
@Test
fun `call foo and bar`() {
wireMockFoo.stubFor(
get(WireMock.urlPathEqualTo("/foo"))
.withQueryParam("name", equalTo(name))
.willReturn(ok().withBody("Hello $name I am Foo!"))
)
wireMockBar.stubFor(
get(WireMock.urlPathMatching("/bar/$name"))
.willReturn(ok().withBody("Hello $name I am Bar!"))
)
val app = App(name, wireMockFoo.baseUrl(), wireMockBar.baseUrl())
assertThat(app.execute()).isEqualTo(
"""
Hi! I am $name
I called Foo and its response is Hello $name I am Foo!
I called Bar and its response is Hello $name I am Bar!
Bye!
""".trimIndent()
)
}
}
App test with WireMock Docker
In our docker-compose.yml:
- We configure two WireMock containers, one for Foo API and one for Bar API.
- We use dynamic ports for each container.
- We enable response templating adding the parameter
--global-response-templating
(see command line options). - We mount as volumes the directories containing the WireMock mappings: foo-api/mappings and bar-api/mappings.
Finally we test the App using Testcontainers JUnit5 extension:
@Testcontainers
@TestInstance(PER_CLASS)
class AppShouldWithWireMockDocker {
private val name = "Ivy"
private val fooServiceName = "foo-api"
private val fooServicePort = 8080
private val barServiceName = "bar-api"
private val barServicePort = 8080
@Container
val container = DockerComposeContainer<Nothing>(File("docker-compose.yml"))
.apply {
withLocalCompose(true)
withExposedService(fooServiceName, fooServicePort, Wait.forListeningPort())
withExposedService(barServiceName, barServicePort, Wait.forListeningPort())
}
@Test
fun `call foo and bar`() {
val fooApiHost = container.getServiceHost(fooServiceName, fooServicePort)
val fooApiPort = container.getServicePort(fooServiceName, fooServicePort)
val barApiHost = container.getServiceHost(barServiceName, barServicePort)
val barApiPort = container.getServicePort(barServiceName, barServicePort)
val fooApiUrl = "http://${fooApiHost}:${fooApiPort}"
val barApiUrl = "http://${barApiHost}:${barApiPort}"
val app = App(name, fooApiUrl, barApiUrl)
assertThat(app.execute()).isEqualTo(
"""
Hi! I am $name
I called Foo and its response is Hello $name I am Foo!
I called Bar and its response is Hello $name I am Bar!
Bye!
""".trimIndent()
)
}
}
With this testing approach we cannot configure our stubs programmatically like we did in testing with @WireMockTest or testing with WireMockExtension. Instead, we have to configure them as json files under mappings directory and we have to use mechanisms such as response templating or stateful behaviour.
App run with WireMock Docker
WireMock with Docker has a cool advantage, we can use the same docker-compose used by the test to start the application and run/debug it locally:
In this case we only need to use fixed ports, configuring them in docker-compose.override.yml. This override does not affect @Testcontainers.
That was a good one! Happy coding!
Test this demo
./gradlew test
Run this demo
docker compose up -d
./gradlew run
docker compose down