diff --git a/README.md b/README.md index 93328e0..d9045ce 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,14 @@ # 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)" 😮 @@ -18,7 +21,9 @@ 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) @@ -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) @@ -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: @@ -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" @@ -326,13 +332,10 @@ class AppShouldWithWireMockDocker { @Container @JvmStatic - val container = DockerComposeContainer(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 @@ -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). @@ -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) diff --git a/build.gradle.kts b/build.gradle.kts index 8984d5b..7c10a14 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") } diff --git a/doc/WireMockDockerTest.png b/doc/WireMockDockerTest.png index 017e83c..4e6f2aa 100644 Binary files a/doc/WireMockDockerTest.png and b/doc/WireMockDockerTest.png differ diff --git a/docker-compose.yml b/docker-compose.yml index e8480f0..c37eb9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: @@ -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: diff --git a/src/test/kotlin/com/rogervinas/wiremock/AppShouldWithWireMockDocker.kt b/src/test/kotlin/com/rogervinas/wiremock/AppShouldWithComposeTestcontainers.kt similarity index 91% rename from src/test/kotlin/com/rogervinas/wiremock/AppShouldWithWireMockDocker.kt rename to src/test/kotlin/com/rogervinas/wiremock/AppShouldWithComposeTestcontainers.kt index 7b8b648..17085f1 100644 --- a/src/test/kotlin/com/rogervinas/wiremock/AppShouldWithWireMockDocker.kt +++ b/src/test/kotlin/com/rogervinas/wiremock/AppShouldWithComposeTestcontainers.kt @@ -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 @@ -15,7 +18,7 @@ import java.io.File @Testcontainers @TestInstance(PER_CLASS) -class AppShouldWithWireMockDocker { +class AppShouldWithComposeTestcontainers { companion object { private const val name = "Ivy" diff --git a/src/test/kotlin/com/rogervinas/wiremock/AppShouldWithWireMockTestcontainers.kt b/src/test/kotlin/com/rogervinas/wiremock/AppShouldWithWireMockTestcontainers.kt new file mode 100644 index 0000000..dbf0a63 --- /dev/null +++ b/src/test/kotlin/com/rogervinas/wiremock/AppShouldWithWireMockTestcontainers.kt @@ -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() + ) + } +}