rogervinas / wiremock-testing

🤹 WireMock Testing

Home Page:https://dev.to/rogervinas/testing-with-wiremocktest-3da0

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

CI Java Kotlin WireMock

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:

(you can also check other supported technologies)

But "talk is cheap, show me the code ..." 😮

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

WireMockTest

And later the one with @Testcontainers and these two alternatives:

  1. Generic Compose Testcontainers module using official WireMock's docker image
  2. WireMock's Testcontainers module

WireMockDockerTest

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 🟩 we can implement the BarKtorClient this way:

class BarKtorClient(private val url: String) : BarClient {

 private val client = HttpClient(CIO) {
   expectSuccess = true
 }

 override fun call(name: String): String = runBlocking {
  try {
   client.get("$url/bar/$name").body<String>()
  } 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(options().globalTemplating(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 using stubFor(...) staticly instead of wm.stubFor(...).

FooKtorClient implementation

Same as before in order to make the test pass 🟩 we can implement the FooKtorClient this way:

class FooKtorClient(private val url: String) : FooClient {
  
 private val client = HttpClient(CIO) {
   expectSuccess = true
 }

 override fun call(name: String): String = runBlocking {
  try {
   client.get("$url/foo") {
    parameter("name", name)
   }.body<String>()
  } 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 Compose Testcontainers module

Static stubs

First we will use static stubs configured as json files:

In our docker-compose.yml:

Finally we test the App using Testcontainers JUnit5 extension:

@Testcontainers
@TestInstance(PER_CLASS)
class AppShouldWithComposeTestcontainers {

 companion object {
  private const val NAME = "Ivy" 
  private const val FOO_SERVICE_NAME = "foo-api"
  private const val FOO_SERVICE_PORT = 8080
  private const val BAR_SERVICE_NAME = "bar-api"
  private const val BAR_SERVICE_PORT = 8080
  private lateinit var fooApiHost: String
  private var fooApiPort: Int = 0
  private lateinit var barApiHost: String
  private var barApiPort: Int = 0
   
  @Container
  @JvmStatic
  val container = ComposeContainer(File("docker-compose.yml"))
    .withLocalCompose(true)
    .withExposedService(FOO_SERVICE_NAME, FOO_SERVICE_PORT, forListeningPort())
    .withExposedService(BAR_SERVICE_NAME, BAR_SERVICE_PORT, forListeningPort())
   
  @BeforeAll
  @JvmStatic
  fun beforeAll() {
    fooApiHost = container.getServiceHost(FOO_SERVICE_NAME, FOO_SERVICE_PORT)
    fooApiPort = container.getServicePort(FOO_SERVICE_NAME, FOO_SERVICE_PORT)
    barApiHost = container.getServiceHost(BAR_SERVICE_NAME, BAR_SERVICE_PORT)
    barApiPort = container.getServicePort(BAR_SERVICE_NAME, BAR_SERVICE_PORT)
  }
 }

 @Test
 fun `call foo and bar`() {
  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()
  )
 }
}

Dynamic stubs

We can also configure our stubs programmatically like we did in testing with @WireMockTest or testing with WireMockExtension.

To do so we have to use the WireMock client and connect it to the WireMock Admin API of the two WireMock containers:

@Test
fun `call foo an bar with dynamic stubs`() {
 val fooApiUrl = "http://${fooApiHost}:${fooApiPort}/dynamic"
 val barApiUrl = "http://${barApiHost}:${barApiPort}/dynamic"

  WireMock(fooApiHost, fooApiPort)
    .register(
      get(urlPathEqualTo("/dynamic/foo"))
        .withQueryParam("name", WireMock.equalTo(name))
        .willReturn(ok().withBody("Hi $name I am Foo, how are you?"))
    )
  WireMock(barApiHost, barApiPort)
    .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()
 )
}

App test with WireMock Testcontainers module

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

@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, 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

App run with WireMock container and Docker Compose

We can use the same docker-compose used by the test to start the application and run/debug it locally:

WireMockDockerRun

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

About

🤹 WireMock Testing

https://dev.to/rogervinas/testing-with-wiremocktest-3da0


Languages

Language:Kotlin 100.0%