If you find yourself in a situation where you need to fetch data using a SOAP web service.. you’re probably in a very bad situation to begin with. But hopefully, I’m here to help!

One of the biggest disadvantages of using SOAP web services is that their support is gradually receding in favour of other protocols. My goal is not to compare SOAP to other messaging protocol alternatives as this has been covered more than throroughly already.

Nevertheless, it does not come as a big surprise that writing tests for a component dependent on SOAP is most of the time painful as well.

I’ve been using Wiremock for quite some time and wondered whether it is possible to create a testing SOAP server based on this project. It is!

The tech stack I’m using is the following:

  • Gradle
  • Kotlin
  • Spring Boot
  • Kotest

I’ve generated JAX-WS files using wsimport, nothing too fancy:

import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
    alias(libs.plugins.spring.boot)
    alias(libs.plugins.spring.dependency.management)
    alias(libs.plugins.jvm)
    alias(libs.plugins.jvm.spring)
    java
}

val jaxwsSourceDir = "${layout.projectDirectory.asFile.absolutePath}/src/main/java"

tasks.getByName<BootJar>("bootJar") {
    enabled = false
}

sourceSets {
    main {
        java {
            srcDirs(jaxwsSourceDir)
        }
    }
}

tasks.register("wsimport") {
    description = "Generate classes from WSDL using wsimport"

    doLast {
        project.mkdir(jaxwsSourceDir)
        ant.withGroovyBuilder {
            "taskdef"("name" to "wsimport",
                "classname" to "com.sun.tools.ws.ant.WsImport",
                "classpath" to configurations["jaxws"].asPath)
            "wsimport"(
                "keep" to true,
                "destdir" to jaxwsSourceDir,
                "extension" to "true",
                "verbose" to true,
                "wsdl" to "${layout.projectDirectory.asFile.absolutePath}/src/main/resources/wsdl/LPI_GDP01B.wsdl",
                "wsdllocation" to "/wsdl/LPI_GDP01B.wsdl",
                "Xnocompile" to true,
                "xadditionalHeaders" to true
            ) {
                "xjcarg"("value" to "-XautoNameResolution")
            }
        }
    }
}

tasks.compileJava {
    dependsOn("wsimport")
}

dependencies {
    "jaxws"(libs.ws)
    "jaxws"(libs.ws.api)
    "jaxws"(libs.ws.bind)
    "jaxws"(libs.ws.rt)
    "jaxws"(libs.ws.activation)

    implementation(libs.ws.bind)
    implementation(libs.ws.api)
}

In case you’re interested what the service does, check out the service documentation (Czech only). The generated files reside in a separate Gradle module (because the generated files are in Java, yay!).

Now, to run an integration test with a SOAP Wiremock server, all I needed to do is to create appropriate WSDL service call stubbings. More on this later.

Now, you might be tempted to use Testcontainers for this, at least I was. As of August 2024, though, the Testcontainers integration of Wiremock is a complete rubbish and a waste of one’s time, so I decided to take the Wiremock setup into my own hands.

Turns out it’s not a complicated matter at all.

Here is my Spring configuration bean, where I manually spin up my Wiremock server (in memory):


import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
import cz.myservice.WsPublicSoilBlocksService
import jakarta.xml.ws.BindingProvider
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import java.net.URI

@Configuration
@ConditionalOnProperty(name = ["wiremock.enabled"], havingValue = "true")
class WiremockWebServicesConfiguration {

    @Bean
    fun wiremockContainer(): WireMockServer =
        WireMockServer(
            WireMockConfiguration
                .options()
                .port(9000)
                .globalTemplating(true)
                .templatingEnabled(true)
                .usingFilesUnderDirectory("src/integrationTest/resources")
        ).apply {
            if(isRunning.not()) {
                start()
            }
        }

    @Bean
    @Primary
    fun wiremockPublicSoilBlocksWs(wireMockContainer: WireMockServer): WsPublicSoilBlocksService {
        // create a WSDL fetch stub
        wireMockContainer.addStubMapping(WireMockContract.PublicSoilBlocks.wsdl())

        val path = publicSoilServicePath(wireMockContainer)
        return WsPublicSoilBlocksService(URI.create(path).toURL())
    }

    private fun publicSoilServicePath(wireMockContainer: WireMockServer): String =
        "${wireMockContainer.baseUrl()}/${WireMockContract.PublicSoilBlocks.SERVICE_CODE}"
}

The reason I need to start the server as soon as possible is that any generated WS service checks that the WSDL location points to a live resource. This check happens inside the service constructor. Ugh.

You might have noticed that the configuration class uses something called WireMockContract. This is a decicated class which tells Wiremock what to do to become a SOAP server. In the Wiremock lingo, we need to define a list of stubbings which map an incoming request to a mocked response. As I’m using HTTP as the underlying transfer protocol of SOAP, stubbings are relatively straightforward:

package cz.myservice.wiremock

import com.github.tomakehurst.wiremock.client.WireMock.*
import com.github.tomakehurst.wiremock.stubbing.StubMapping
import org.springframework.http.HttpStatus

class WireMockContract {

    companion object PublicSoilBlocks {
        const val SERVICE_CODE = "LPI_GDP01B"

        fun wsdl(): StubMapping =
            soilBlockWsdlRequest()
                .willReturn(soilBlocksWsdl())
                .build()

        fun request(iddpb: Int): StubMapping =
            soilBlockRequest(iddpb)
                .willReturn(soilBlock(iddpb))
                .build()

        private fun soilBlock(iddpb: Int) =
            aResponse()
                .withStatus(HttpStatus.OK.value())
                .withBodyFile("$SERVICE_CODE/$iddpb.xml")

        private fun soilBlocksWsdl() =
            aResponse()
                .withStatus(HttpStatus.OK.value())
                .withBody(requireNotNull(javaClass.getResource("/wsdl/$SERVICE_CODE.wsdl")).readText())

        private fun soilBlockRequest(iddpb: Int) =
            post("/ws/$SERVICE_CODE").withRequestBody(matchingXPath("//KUKOD/text()", matching(iddpb.toString())))

        private fun soilBlockWsdlRequest() =
            get("/$SERVICE_CODE")
    }
}

These StubMapping instances do not do anything interesting by themselves, we need to connect them to our running Wiremock server to activate them. This is what an integration test might look like:

package cz.myservice

import cz.myservice.configuration.properties.BeehiveExternalConfigurationProperties
import cz.myservice.eagri.block.LpisSoilBlockImportService
import cz.myservice.wiremock.BeehiveWireMockContract
import cz.myservice.wiremock.WiremockSpec
import io.kotest.matchers.shouldNotBe

internal class BeehiveIT(
    private val importService: LpisSoilBlockImportService,
    private val properties: BeehiveExternalConfigurationProperties
): WiremockSpec() {

    init {
        should("should insert new blocks") {
            // create service call stubs
            server.addStubMapping(BeehiveWireMockContract.PublicSoilBlocks.request(12345))

            // run the logic
            importService.updateSoilBlocks() shouldNotBe null
        }
    }
}

@TestPropertySource(properties = ["wiremock.enabled=true"])
@EnableAutoConfiguration
@Import(WiremockWebServicesConfiguration::class)
abstract class WiremockSpec : IntegrationSpec() {

    @Autowired
    protected lateinit var server: WireMockServer

    init {
        beforeEach {
            server.resetAll()
        }
        afterSpec {
            if(server.isRunning) {
                server.stop()
            }
        }
    }
}