{"id":15403644,"url":"https://github.com/rogervinas/mockserver-testing","last_synced_at":"2025-04-16T03:44:34.688Z","repository":{"id":115435652,"uuid":"497040432","full_name":"rogervinas/mockserver-testing","owner":"rogervinas","description":"🎭 MockServer Testing","archived":false,"fork":false,"pushed_at":"2025-03-26T13:27:28.000Z","size":742,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-26T14:40:37.084Z","etag":null,"topics":["kotlin","mockserver","testcontainers","testing"],"latest_commit_sha":null,"homepage":"https://dev.to/rogervinas/testing-with-mockserver-1cao","language":"Kotlin","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rogervinas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-27T15:06:31.000Z","updated_at":"2025-03-26T13:27:31.000Z","dependencies_parsed_at":"2023-11-12T09:22:48.154Z","dependency_job_id":"43fb6dbf-8381-4e07-906b-850b7804f7ab","html_url":"https://github.com/rogervinas/mockserver-testing","commit_stats":{"total_commits":70,"total_committers":3,"mean_commits":"23.333333333333332","dds":0.2571428571428571,"last_synced_commit":"6a9515a8359f6490a08eabb7327b54a56a9a954a"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fmockserver-testing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fmockserver-testing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fmockserver-testing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fmockserver-testing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rogervinas","download_url":"https://codeload.github.com/rogervinas/mockserver-testing/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249192000,"owners_count":21227701,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kotlin","mockserver","testcontainers","testing"],"created_at":"2024-10-01T16:09:31.496Z","updated_at":"2025-04-16T03:44:34.667Z","avatar_url":"https://github.com/rogervinas.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![CI](https://github.com/rogervinas/mockserver-testing/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/rogervinas/mockserver-testing/actions/workflows/ci.yml)\n![Java](https://img.shields.io/badge/Java-21-blue?labelColor=black)\n![Kotlin](https://img.shields.io/badge/Kotlin-2.x-blue?labelColor=black)\n![MockServer](https://img.shields.io/badge/MockServer-5.15.0-blue?labelColor=black)\n\n# MockServer Testing\n\nI've just found out [MockServer](https://mock-server.com/) and it looks awesome 🤩 so I wanted to check it out repeating the steps of my previous demo [WireMock Testing](https://github.com/rogervinas/wiremock-testing) which (as you can expect) uses [WireMock](https://wiremock.org/), another fantastic tool to mock APIs.\n\nSo in this demo we will use first [MockServer JUnit5 extension](https://mock-server.com/mock_server/running_mock_server.html#junit_test_extension):\n\n![MockServerTest](doc/MockServerTest.png)\n\nAnd then we will use [MockServer Docker image](https://mock-server.com/mock_server/running_mock_server.html#docker_container) and [Testcontainers](https://www.testcontainers.org/):\n\n![MockServerDockerTest](doc/MockServerDockerTest.png)\n\n* [BarClient](#barclient)\n  * [BarClient interface](#barclient-interface)\n  * [BarKtorClient test](#barktorclient-test)\n  * [BarKtorClient implementation](#barktorclient-implementation)\n* [FooClient](#fooclient)\n  * [FooClient interface](#fooclient-interface)\n  * [FooKtorClient test](#fooktorclient-test)\n  * [FooKtorClient implementation](#fooktorclient-implementation)\n* [AppUseCase](#appusecase)\n* [App](#app)\n  * [App implementation](#app-implementation)\n  * [App test with MockServerExtension](#app-test-with-mockserverextension)\n  * [App test with two MockServers](#app-test-with-two-mockservers)\n  * [App test with MockServer Docker](#app-test-with-mockserver-docker)\n    * [Static stubs](#static-stubs)\n    * [Dynamic stubs](#dynamic-stubs)\n  * [App run with MockServer Docker](#app-run-with-mockserver-docker)\n* [Test this demo](#test-this-demo)\n* [Run this demo](#run-this-demo)\n\n## BarClient\n\n### BarClient interface\n\n```kotlin\ninterface BarClient {\n  fun call(name: String): String\n}\n```\n\n### BarKtorClient test\n\nI will use a [Ktor client](https://ktor.io/docs/client.html) for no other reason that I need an Http client and this seems interesting, as we are using **Kotlin**.\n\nSo a simple test with **MockServerExtension** for the **BarKtorClient** looks like:\n\n```kotlin\n@ExtendWith(MockServerExtension::class)\n@TestInstance(PER_CLASS)\nclass BarKtorClientShould {\n\n  private val name = \"Sue\"\n\n  private lateinit var mockServerClient: MockServerClient\n  private lateinit var mockServerUrl: String\n\n  @BeforeAll\n  fun beforeAll(mockServerClient: MockServerClient) {\n    this.mockServerClient = mockServerClient\n    this.mockServerUrl = \"http://localhost:${mockServerClient.port}\"\n  }\n\n  @BeforeEach\n  fun beforeEach() {\n    mockServerClient.reset()\n  }\n\n  @Test\n  fun `call bar api`() {\n    mockServerClient\n      .`when`(request().withMethod(\"GET\").withPath(\"/bar/${name}\"))\n      .respond(response().withStatusCode(200).withBody(\"Hello $name I am Bar!\"))\n\n    assertThat(BarKtorClient(mockServerUrl).call(name))\n      .isEqualTo(\"Hello $name I am Bar!\")\n  }\n\n  @Test\n  fun `handle bar api server error`() {\n    mockServerClient\n      .`when`(request().withMethod(\"GET\").withPath(\"/bar/.+\"))\n      .respond(response().withStatusCode(503))\n\n    assertThat(BarKtorClient(mockServerUrl).call(name))\n      .startsWith(\"Bar api error: Server error\")\n  }\n}\n```\n\nNote that we can inject **MockServerClient** as a parameter in the test methods too.\n\n### BarKtorClient implementation\n\nIn order to make the test pass 🟩 we can implement the **BarKtorClient** this way:\n\n```kotlin\nclass BarKtorClient(private val url: String) : BarClient {\n\n  private val client = HttpClient(CIO)\n\n  override fun call(name: String): String = runBlocking {\n    try {\n      client.get(\"$url/bar/$name\")\n    } catch (e: Exception) {\n      \"Bar api error: ${e.message}\"\n    }\n  }\n}\n```\n\n## FooClient\n\n### FooClient interface\n\n```kotlin\ninterface FooClient {\n  fun call(name: String): String\n}\n```\n\n### FooKtorClient test\n\nFor this test we will use [MockServer's Mustache templates](https://mock-server.com/mock_server/response_templates.html#mustache_templates):\n\n```kotlin\n@TestInstance(PER_CLASS)\nclass FooKtorClientShould {\n\n  private val name = \"Joe\"\n\n  private lateinit var mockServerClient: MockServerClient\n  private lateinit var mockServerUrl: String\n\n  @BeforeAll\n  fun beforeAll() {\n    mockServerClient = ClientAndServer()\n    mockServerUrl = \"http://localhost:${mockServerClient.port}\"\n  }\n\n  @BeforeEach\n  fun beforeEach() {\n    mockServerClient.reset()\n  }\n\n  @Test\n  fun `call foo api`() {\n    mockServerClient\n      .`when`(request().withMethod(\"GET\").withPath(\"/foo\").withQueryStringParameter(\"name\", \".+\"))\n      .respond(template(\n        MUSTACHE,\n        \"\"\"\n        {\n          statusCode: 200,\n          body: 'Hello {{ request.queryStringParameters.name.0 }} I am Foo!'\n        }\n        \"\"\".trimIndent()\n      ))\n\n    assertThat(FooKtorClient(mockServerUrl).call(name)).isEqualTo(\"Hello $name I am Foo!\")\n  }\n\n  @Test\n  fun `handle foo api server error`() {\n    mockServerClient\n      .`when`(request().withMethod(\"GET\").withPath(\"/foo\").withQueryStringParameter(\"name\", \".+\"))\n      .respond(response().withStatusCode(503))\n\n    assertThat(FooKtorClient(mockServerUrl).call(name)).startsWith(\"Foo api error: Server error\")\n  }\n}\n```\n\nNote that:\n* As in the previous test we can inject **MockServerClient** as a parameter in the test methods too.\n* **MockServer** provides also [Velocity](https://mock-server.com/mock_server/response_templates.html#velocity_templates) and [Javascript](https://mock-server.com/mock_server/response_templates.html#javascript_templates) templates that support more complex logic.\n\n### FooKtorClient implementation\n\nSame as before in order to make the test pass 🟩 we can implement the **FooKtorClient** this way:\n\n```kotlin\nclass FooKtorClient(private val url: String) : FooClient {\n\n  private val client = HttpClient(CIO)\n\n  override fun call(name: String): String = runBlocking {\n    try {\n      client.get(\"$url/foo\") {\n        parameter(\"name\", name)\n      }\n    } catch (e: Exception) {\n      \"Foo api error: ${e.message}\"\n    }\n  }\n}\n```\n\n## AppUseCase\n\nNow we have to implement **AppUseCase**, which will use a **FooClient** to call the **Foo API** and then a **BarClient** to call the **Bar API**. \n\nAs it is not **MockServer** related because we can test first the implementation just using [MockK JUnit5 extension](https://mockk.io/#junit5) we can skip the details and you can review the source code of [AppUseCaseShould](src/test/kotlin/com/rogervinas/mockserver/AppUseCaseShould.kt) and [AppUseCase](src/main/kotlin/com/rogervinas/mockserver/AppUseCase.kt).\n\n## App\n\n### App implementation\n\nLet me introduce first the **App** implementation, as I will present later two different types of **MockServer** tests:\n\n```kotlin\nclass App(\n  private val name: String,\n  private val fooApiUrl: String,\n  private val barApiUrl: String\n) {\n\n  fun execute() = AppUseCase().execute(\n    name,\n    FooKtorClient(fooApiUrl),\n    BarKtorClient(barApiUrl)\n  )\n}\n```\n\n### App test with MockServerExtension\n\nSince in this example **Foo API** and **Bar API** \u003cu\u003edo not have conflicting endpoints\u003c/u\u003e, we can use **MockServerExtension** to mock both APIs:\n\n```kotlin\n@ExtendWith(MockServerExtension::class)\nclass AppShouldWithOneMockServer {\n\n  private val name = \"Ada\"\n\n  @Test\n  fun `call foo and bar`(mockServerClient: MockServerClient) {\n    mockServerClient\n      .`when`(request().withMethod(\"GET\").withPath(\"/foo\").withQueryStringParameter(\"name\", name))\n      .respond(response().withStatusCode(200).withBody(\"Hello ${name} I am Foo!\"))\n    mockServerClient\n      .`when`(request().withMethod(\"GET\").withPath(\"/bar/${name}\"))\n      .respond(response().withStatusCode(200).withBody(\"Hello $name I am Bar!\"))\n\n    val mockServerUrl = \"http://localhost:${mockServerClient.port}\"\n    val app = App(name, mockServerUrl, mockServerUrl)\n\n    assertThat(app.execute()).isEqualTo(\n      \"\"\"\n        Hi! I am $name\n        I called Foo and its response is Hello $name I am Foo!\n        I called Bar and its response is Hello $name I am Bar!\n        Bye!\n      \"\"\".trimIndent()\n    )\n  }\n}\n```\n\n### App test with two MockServers\n\nBut imagine a real scenario where **Foo API** and **Bar API** \u003cu\u003edo have conflicting endpoints\u003c/u\u003e, or you just want to \u003cu\u003emock them separatedly for any reason\u003c/u\u003e. In this case you can use two **MockServers** instead of using **MockServerExtension**:\n\n```kotlin\n@TestInstance(PER_CLASS)\nclass AppShouldWithTwoMockServers {\n\n  private val name = \"Leo\"\n\n  private val mockServerClientFoo = ClientAndServer()\n  private val mockServerClientBar = ClientAndServer()\n\n  @Test\n  fun `call foo and bar`() {\n    mockServerClientFoo\n      .`when`(request().withMethod(\"GET\").withPath(\"/foo\").withQueryStringParameter(\"name\", name))\n      .respond(response().withStatusCode(200).withBody(\"Hello ${name} I am Foo!\"))\n    mockServerClientBar\n      .`when`(request().withMethod(\"GET\").withPath(\"/bar/${name}\"))\n      .respond(response().withStatusCode(200).withBody(\"Hello $name I am Bar!\"))\n\n    val mockServerFooUrl = \"http://localhost:${mockServerClientFoo.port}\"\n    val mockServerBarUrl = \"http://localhost:${mockServerClientBar.port}\"\n    val app = App(name, mockServerFooUrl, mockServerBarUrl)\n\n    assertThat(app.execute()).isEqualTo(\n      \"\"\"\n        Hi! I am $name\n        I called Foo and its response is Hello $name I am Foo!\n        I called Bar and its response is Hello $name I am Bar!\n        Bye!\n      \"\"\".trimIndent()\n    )\n  }\n}\n```\n\n## App test with MockServer Docker\n\n### Static stubs\n\nFirst we will use static stubs configured as json files:\n\nIn our [docker-compose.yml](docker-compose.yml):\n* We configure two **MockServer** containers, one for **Foo API** and one for **Bar API**.\n* We use dynamic ports for each container.\n* We load [persisted expectations](https://mock-server.com/mock_server/persisting_expectations.html) at startup setting `MOCKSERVER_INITIALIZATION_JSON_PATH` variable.\n* We mount as volumes the directories containing the **MockServer** expectations: [foo-api/mockserver.json](mockserver/foo-api/mockserver.json) and [bar-api/mockserver.json](mockserver/bar-api/mockserver.json).\n\nFinally we test the **App** using [Testcontainers JUnit5 extension](https://www.testcontainers.org/test_framework_integration/junit_5/):\n\n```kotlin\n@Testcontainers\n@TestInstance(PER_CLASS)\nclass AppShouldWithMockServerDocker {\n\n  companion object {\n    private const val name = \"Ivy\"\n\n    private const val fooServiceName = \"foo-api\"\n    private const val fooServicePort = 8080\n    private const val barServiceName = \"bar-api\"\n    private const val barServicePort = 8080\n\n    private lateinit var fooApiHost: String\n    private var fooApiPort: Int = 0\n    private lateinit var barApiHost: String\n    private var barApiPort: Int = 0\n\n    val waitForMockServerLiveness = forHttp(\"/mockserver/status\")\n      .withMethod(\"PUT\")\n      .forStatusCode(200)\n\n    @Container\n    @JvmStatic\n    val container = ComposeContainer(File(\"docker-compose.yml\"))\n      .withLocalCompose(true)\n      .withExposedService(fooServiceName, fooServicePort, waitForMockServerLiveness)\n      .withExposedService(barServiceName, barServicePort, waitForMockServerLiveness)\n\n    @BeforeAll\n    @JvmStatic\n    fun beforeAll() {\n      fooApiHost = container.getServiceHost(fooServiceName, fooServicePort)\n      fooApiPort = container.getServicePort(fooServiceName, fooServicePort)\n      barApiHost = container.getServiceHost(barServiceName, barServicePort)\n      barApiPort = container.getServicePort(barServiceName, barServicePort)\n    }\n  }\n\n  @Test\n  fun `call foo and bar`() {\n    val fooApiUrl = \"http://${fooApiHost}:${fooApiPort}\"\n    val barApiUrl = \"http://${barApiHost}:${barApiPort}\"\n\n    val app = App(name, fooApiUrl, barApiUrl)\n\n    assertThat(app.execute()).isEqualTo(\n      \"\"\"\n        Hi! I am $name\n        I called Foo and its response is Hello $name I am Foo!\n        I called Bar and its response is Hello $name I am Bar!\n        Bye!\n      \"\"\".trimIndent()\n    )\n  }\n}\n```\n\n### Dynamic stubs\n\nWe can also configure our stubs programmatically using the **MockServerClient** and connect it to each one of the two **MockServer** containers:\n\n```kotlin\n@Test\nfun `call foo an bar with dynamic stubs`() {\n  val fooApiUrl = \"http://${fooApiHost}:${fooApiPort}/dynamic\"\n  val barApiUrl = \"http://${barApiHost}:${barApiPort}/dynamic\"\n\n  MockServerClient(fooApiHost, fooApiPort)\n    .`when`(\n      request()\n        .withMethod(\"GET\")\n        .withPath(\"/dynamic/foo\")\n        .withQueryStringParameter(\"name\", name)\n    )\n    .respond(\n      response()\n        .withStatusCode(200)\n        .withBody(\"Hi $name I am Foo, how are you?\")\n    )\n  MockServerClient(barApiHost, barApiPort)\n    .`when`(\n      request()\n        .withMethod(\"GET\")\n        .withPath(\"/dynamic/bar/$name\")\n    ).respond(\n      response()\n        .withStatusCode(200)\n        .withBody(\"Hi $name I am Bar, nice to meet you!\")\n    )\n\n  val app = App(name, fooApiUrl, barApiUrl)\n\n  assertThat(app.execute()).isEqualTo(\n    \"\"\"\n      Hi! I am $name\n      I called Foo and its response is Hi $name I am Foo, how are you?\n      I called Bar and its response is Hi $name I am Bar, nice to meet you!\n      Bye!\n    \"\"\".trimIndent()\n  )\n}\n```\n\n## App run with MockServer Docker\n\n**MockServer** 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:\n\n![MockServerDockerRun](doc/MockServerDockerRun.png)\n\nIn this case we only need to use fixed ports, configuring them in [docker-compose.override.yml](docker-compose.override.yml). This override does not affect **@Testcontainers**.\n\nThat was a good one! Happy coding! 💙\n\n## Test this demo\n\n```shell\n./gradlew test\n```\n\n## Run this demo\n\n```\ndocker compose up -d\n./gradlew run\ndocker compose down\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogervinas%2Fmockserver-testing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frogervinas%2Fmockserver-testing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogervinas%2Fmockserver-testing/lists"}