Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use WireMock Testcontainers #24

Merged
merged 15 commits into from
Nov 7, 2023
80 changes: 62 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,24 @@
# WireMock Testing

[WireMock](https://wiremock.org/) is a great library to mock APIs in your tests and supports [Junit5](https://wiremock.org/docs/junit-jupiter/) with two modes:

- Declarative with **@WireMockTest**
- Programmatic with **WireMockExtension**

And **WireMock** also has an [official Docker image](https://hub.docker.com/r/wiremock/wiremock)!
And **WireMock** also has:
- A [Docker image](https://hub.docker.com/r/wiremock/wiremock)!
- A [Testcontainers module](https://wiremock.org/docs/solutions/testcontainers/)!

(you can also check [other supported technologies](https://wiremock.org/docs/#:~:text=By%20technology))

But "talk is cheap, show me the code [...](https://www.goodreads.com/quotes/437173-talk-is-cheap-show-me-the-code#:~:text=Quote%20by%20Linus%20Torvalds%3A%20%E2%80%9CTalk,Show%20me%20the%20code.%E2%80%9D)" 😮

Ok so let's implement first the scenario with **@WireMockTest**:

![WireMockTest](doc/WireMockTest.png)

And later the one with [WireMock's official Docker image](https://hub.docker.com/r/wiremock/wiremock):
And later the one with [@Testcontainers](https://testcontainers.com/) and these two alternatives:
1. Generic [Compose Testcontainers module](https://java.testcontainers.org/modules/docker_compose/#compose-v2) using [official WireMock's docker image](https://hub.docker.com/r/wiremock/wiremock)
2. [WireMock's Testcontainers module](https://wiremock.org/docs/solutions/testcontainers/)

![WireMockDockerTest](doc/WireMockDockerTest.png)

Expand All @@ -35,10 +40,11 @@ And later the one with [WireMock's official Docker image](https://hub.docker.com
* [App implementation](#app-implementation)
* [App test with @WireMockTest](#app-test-with-wiremocktest)
* [App test with WireMockExtension](#app-test-with-wiremockextension)
* [App test with WireMock Docker](#app-test-with-wiremock-docker)
* [App test with Compose Testcontainers module](#app-test-with-compose-testcontainers-module)
* [Static stubs](#static-stubs)
* [Dynamic stubs](#dynamic-stubs)
* [App run with WireMock Docker](#app-run-with-wiremock-docker)
* [App test with WireMock Testcontainers module](#app-test-with-wiremock-testcontainers-module)
* [App run with WireMock container and Docker Compose](#app-run-with-wiremock-container-and-docker-compose)
* [Test this demo](#test-this-demo)
* [Run this demo](#run-this-demo)

Expand Down Expand Up @@ -294,9 +300,9 @@ class AppShouldWithTwoWireMockExtensions {
}
```

## App test with WireMock Docker
### App test with Compose Testcontainers module

### Static stubs
#### Static stubs

First we will use static stubs configured as json files:

Expand All @@ -311,7 +317,7 @@ Finally we test the **App** using [Testcontainers JUnit5 extension](https://www.
```kotlin
@Testcontainers
@TestInstance(PER_CLASS)
class AppShouldWithWireMockDocker {
class AppShouldWithComposeTestcontainers {

companion object {
private const val name = "Ivy"
Expand All @@ -326,13 +332,10 @@ class AppShouldWithWireMockDocker {

@Container
@JvmStatic
val container = DockerComposeContainer<Nothing>(File("docker-compose.yml"))
.apply {
withLocalCompose(true)
withExposedService(fooServiceName, fooServicePort, forListeningPort())
withExposedService(barServiceName, barServicePort, forListeningPort())
withLogConsumer()
}
val container = ComposeContainer(File("docker-compose.yml"))
.withLocalCompose(true)
.withExposedService(fooServiceName, fooServicePort, forListeningPort())
.withExposedService(barServiceName, barServicePort, forListeningPort())

@BeforeAll
@JvmStatic
Expand Down Expand Up @@ -363,7 +366,7 @@ class AppShouldWithWireMockDocker {
}
```

### Dynamic stubs
#### Dynamic stubs

We can also configure our stubs programmatically like we did in [testing with @WireMockTest](#app-test-with-wiremocktest) or [testing with WireMockExtension](#app-test-with-wiremockextension).

Expand Down Expand Up @@ -397,9 +400,50 @@ fun `call foo an bar with dynamic stubs`() {
}
```

## App run with WireMock Docker
### App test with WireMock Testcontainers module

Instead of the generic **ComposeContainer** we can use the specific **WireMockContainer** this way:

```kotlin
@Testcontainers
@TestInstance(PER_CLASS)
class AppShouldWithWireMockTestcontainers {

companion object {
@Container
@JvmStatic
val containerFoo = WireMockContainer("wiremock/wiremock:3.2.0")
.withMappingFromJSON(File("wiremock/foo-api/mappings/foo-get.json").readText())
.withCliArg("--global-response-templating")

@Container
@JvmStatic
val containerBar = WireMockContainer("wiremock/wiremock:3.2.0")
.withMappingFromJSON(File("wiremock/bar-api/mappings/bar-get.json").readText())
.withCliArg("--global-response-templating")
}

@Test
fun `call foo and bar`() {
val fooApiUrl = "http://${containerFoo.host}:${containerFoo.port}"
val barApiUrl = "http://${containerBar.host}:${containerBar.port}"
// ...
}

@Test
fun `call foo an bar with dynamic stubs`() {
// ...
}
}
```

Tests are the same as the ones in [App test with Compose Testcontainers module](#app-test-with-compose-testcontainers-module), just with two minor differences:
- The way we get `host` and `port` for each container
- The way we specify `--global-response-templating` parameter to enable [response templating](https://wiremock.org/docs/response-templating/)

### App run with WireMock container and Docker Compose

**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:
We can use the same **docker-compose** used by the test to start the application and run/debug it locally:

![WireMockDockerRun](doc/WireMockDockerRun.png)

Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
testImplementation("io.mockk:mockk:1.12.2")
testImplementation("org.assertj:assertj-core:3.24.2")
testImplementation("org.wiremock:wiremock:3.3.1")
testImplementation("org.wiremock.integrations.testcontainers:wiremock-testcontainers-module:1.0-alpha-13")
testImplementation("org.testcontainers:testcontainers:1.19.1")
testImplementation("org.testcontainers:junit-jupiter:1.19.1")
}
Expand Down
Binary file modified doc/WireMockDockerTest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: "3.9"
services:

foo-api:
image: wiremock/wiremock:2.32.0
image: wiremock/wiremock:3.2.0
ports:
- "8080"
command:
Expand All @@ -12,7 +12,7 @@ services:
- ./wiremock/foo-api:/home/wiremock

bar-api:
image: wiremock/wiremock:2.32.0
image: wiremock/wiremock:3.2.0
ports:
- "8080"
command:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.rogervinas.wiremock

import com.github.tomakehurst.wiremock.client.WireMock
import com.github.tomakehurst.wiremock.client.WireMock.*
import com.github.tomakehurst.wiremock.client.WireMock.get
import com.github.tomakehurst.wiremock.client.WireMock.ok
import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo
import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
Expand All @@ -15,7 +18,7 @@ import java.io.File

@Testcontainers
@TestInstance(PER_CLASS)
class AppShouldWithWireMockDocker {
class AppShouldWithComposeTestcontainers {

companion object {
private const val name = "Ivy"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.rogervinas.wiremock

import com.github.tomakehurst.wiremock.client.WireMock
import com.github.tomakehurst.wiremock.client.WireMock.get
import com.github.tomakehurst.wiremock.client.WireMock.ok
import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo
import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.wiremock.integrations.testcontainers.WireMockContainer
import java.io.File

@Testcontainers
@TestInstance(PER_CLASS)
class AppShouldWithWireMockTestcontainers {

companion object {
private const val name = "Ivy"

@Container
@JvmStatic
val containerFoo = WireMockContainer("wiremock/wiremock:3.2.0")
.withMappingFromJSON(File("wiremock/foo-api/mappings/foo-get.json").readText())
.withCliArg("--global-response-templating")

@Container
@JvmStatic
val containerBar = WireMockContainer("wiremock/wiremock:3.2.0")
.withMappingFromJSON(File("wiremock/bar-api/mappings/bar-get.json").readText())
.withCliArg("--global-response-templating")
}

@Test
fun `call foo and bar`() {
val fooApiUrl = "http://${containerFoo.host}:${containerFoo.port}"
val barApiUrl = "http://${containerBar.host}:${containerBar.port}"

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()
)
}

@Test
fun `call foo an bar with dynamic stubs`() {
val fooApiUrl = "http://${containerFoo.host}:${containerFoo.port}/dynamic"
val barApiUrl = "http://${containerBar.host}:${containerBar.port}/dynamic"

WireMock(containerFoo.host, containerFoo.port)
.register(get(urlPathEqualTo("/dynamic/foo"))
.withQueryParam("name", WireMock.equalTo(name))
.willReturn(ok().withBody("Hi $name I am Foo, how are you?"))
)
WireMock(containerBar.host, containerBar.port)
.register(get(urlPathMatching("/dynamic/bar/$name"))
.willReturn(ok().withBody("Hi $name I am Bar, nice to meet you!"))
)

val app = App(name, fooApiUrl, barApiUrl)

assertThat(app.execute()).isEqualTo(
"""
Hi! I am $name
I called Foo and its response is Hi $name I am Foo, how are you?
I called Bar and its response is Hi $name I am Bar, nice to meet you!
Bye!
""".trimIndent()
)
}
}