/spring-wiremock-stub-generator

A tool for generating Wiremock stubs from Spring MVC controllers

Primary LanguageKotlinMIT LicenseMIT

semantic-release

spring-wiremock-stub-generator

GitHub Codecov

CI Nightly Build GitHub release Maven Central

About

This is a tool for generating Wiremock stubs from Spring @Controller & @RestController annotated classes

WireMock stub generation diagram

Applicability

The WireMock generator has the following constraints and limitations:

  • the request and response classes are shared between the producer and the consumers of the API
  • currently, the generator supports only JSON based communication

How to use it

Add the following dependencies to a Java project:

    annotationProcessor 'io.github.lsd-consulting:spring-wiremock-stub-generator:x.x.x'
    compileOnly 'io.github.lsd-consulting:spring-wiremock-stub-generator:x.x.x'
    compileOnly 'org.wiremock:wiremock:3.5.2'

or these for a Kotlin project:

    kapt 'io.github.lsd-consulting:spring-wiremock-stub-generator:x.x.x'
    compileOnly 'io.github.lsd-consulting:spring-wiremock-stub-generator:x.x.x'
    compileOnly 'org.wiremock:wiremock:3.5.2'

The above will set up the annotation processor which will analyse the source code and generate the Java WireMock stubs.

Generated Java WireMock stub

To compile the stubs add the following:

task compileStubs(type: JavaCompile) {
    JavaCompile compileJava = project.getTasksByName("compileJava", true).toArray()[0]
    classpath = compileJava.classpath
    source = project.getLayout().getBuildDirectory().dir("generated-stub-sources")
    def stubsClassesDir = file("${project.getBuildDir()}/generated-stub-classes")
    destinationDir(stubsClassesDir)
    compileJava.finalizedBy(compileStubs)
}

The result would be a class file(s) like this: Compiled Java WireMock stub

And to build a JAR file with the stubs:

task stubsJar(type: Jar) {
    JavaCompile compileJavaStubs = project.getTasksByName("compileStubs", true).toArray()[0]
    setDescription('Java Wiremock stubs JAR')
    setGroup("Verification")
    archiveBaseName.convention(project.provider(project::getName))
    archiveClassifier.convention("wiremock-stubs")
    from(compileJavaStubs.getDestinationDirectory())
    dependsOn(compileJavaStubs)
    compileJavaStubs.finalizedBy(stubsJar)
    project.artifacts(artifactHandler -> artifactHandler.add("archives", stubsJar))
}

WireMock stub jar

The JAR file can then be published as an artifact:

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java

            artifact stubsJar
            artifactId "${rootProject.name}-${project.name}"
        }
    }
}

The last step is to import it as a dependency in another project and use it.

testImplementation('group:artifact:+:wiremock-stubs') {
        exclude group: "*", module: "*"
    }

NOTE: The exclusion is necessary if the published artifact imports transitively all the producer's dependencies.

Examples:

For Java and Kotlin examples please check out the following project:

https://github.com/lsd-consulting/spring-wiremock-stub-generator-example

Multi-value request parameters

The problem

According to the accepted answer here and this Wiki page :

While there is no definitive standard, most web frameworks allow multiple values to be associated with a single field (e.g. field1=value1&field1=value2&field2=value3).

However, according to this thread, WireMock doesn't support duplicate query names.

Since Spring MVC does provide handling of duplicate query params out of the box, eg.

    @GetMapping("/resourceWithParamSet")
    fun resourceWithParamSet(@RequestParam paramSet: Set<String>) {
        ...
    }

there is no easy way to set up a WireMock stub for the above, or similar, examples.

The implemented solution

The library handles queries like this in a special way.

As soon as a multi-value query parameter is detected, the WireMock stub is switched from urlPathEqualTo matcher, to the urlEqualTo one. The implication is that the ordering of the request parameters becomes significant.

Therefore, it is important to use ordered collections when sending multi-value parameters in tests to make the queries deterministic.

Optional request parameters

Optional request parameters is another feature of the HTTP protocol that is not yet supported by WireMock (see here).

To handle the following SpringMVC definition:

    @GetMapping("/resourceWithOptionalBooleanRequestParam")
    fun resourceWithOptionalBooleanRequestParam(@RequestParam(required = false) param: Boolean) {
        ...
    }

the library introduces conditions into the generated stub.

If a parameter is optional and the value passed in is a null, the generated stub will not add the query param matcher to the Wiremock stub.

This means that the following call to the generated stub:

underTest.resourceWithOptionalBooleanRequestParam(response, null)

will generate a WireMock matcher to match the following GET request:

/resourceWithOptionalBooleanRequestParam

But if a value is passed in that call, eg:

underTest.resourceWithOptionalBooleanRequestParam(response, "value")

then WireMock will expect the following request:

/resourceWithOptionalBooleanRequestParam?param=value

Handling @DateTimeFormat

If the request parameter or path variable is a date, it needs to be annotated with @DateTimeFormat, eg:

@GetMapping("/resource") 
fun resource(@RequestParam @DateTimeFormat(iso = DATE_TIME) timestamp: ZonedDateTime)

For the generated stub object to be able to handle such values, it needs an AnnotationFormatterFactory, eg. Jsr310DateTimeFormatAnnotationFormatterFactory.

A special constructor needs to be used when creating an instance of the stub:

RestControllerStub(ObjectMapper(), Jsr310DateTimeFormatAnnotationFormatterFactory())

Contributing

We welcome bug fixes and new features in the form of pull requests. If you'd like to contribute, please be mindful of the following guidelines:

  • Start with raising an issue
  • All changes should include suitable tests, whether to demonstrate the bug or exercise and document the new feature.
  • Please make one change per pull request.
  • Use conventional commits
  • If the new feature is significantly large/complex/breaks existing behaviour, please first post a summary on the issue to generate a discussion. This will avoid significant amounts of coding time spent on changes that ultimately get rejected.
  • Try to avoid reformats of files that change the indentation, spaces to tabs etc., as this makes reviewing diffs much more difficult.

Preferred approach

Probably the best way to add functionality to this project is through tests.

Here is an example commit of adding support for Kotlin vararg.

That commit doesn't show how the change actually came about, but here are the steps that were taken in the following order:

  1. Add a new resource to a controller in the integration tests
  2. Build the project so that the stubs are updated
  3. Add a failing integration test
  4. Make a change in the library that makes the test pass

In the majority of cases following the above steps should suffice.