Skip to content

Masabi/swagwire

Repository files navigation

Build Status

Latest version 1.0.10-openapi3

Swagwire

Introduction

Swagwire is a project that combines OpenApi 3 and Wiremock to ease the burden when writing tests against APIs.

Wiremock is that de facto tool for writing test code that goes over the wire, but writing tests is not only cumbersome, it requires knowledge of how to interact with the API you're working with. Not only does this open you up to error, but offers no guarantees that a passing test means your integration is correct.

If we take the canonical petstore example, a traditional Wiremock based test might look something like this:

class PetstoreSpec extends Specification {
    @Rule
    WireMockRule wireMockRule = new WireMockRule(0)

    def setup() {
        WireMock.configureFor(wireMockRule.port())
    }

    def "can fetch a pet"() {
        given:
            stubFor(get(urlMatching("/pet/1234"))
                .willReturn(aResponse()
                    .withHeader("Content-Type", "application/json")
                    .withStatus(200)
                    .withBody("""{
                      "name": "Rover",
                      "photoUrls": [ "http://url1", "http://url2" ]      
                    }""")
            ))

        expect:
            fetchingPet("1234").name == "Rover"
    }

They key things here are the knowledge of the URL structure and the format of the pet body. It's all too easy to make a mistake here and replicate it in production code, ending up with green tests that verify you made the same mistake twice!

Swagwire helps to overcome this by generating code stubs for the Wiremock bindings, in a similar way to how you might use it for production generation. After running the generator, you can change the above test to the following:

    @Rule
    WireMockRule wireMockRule = new WireMockRule(0)

    def "can fetch a pet"() {
        given:
            SwagWiredServiceConfiguration configuration = SwagWiredServiceConfiguration.builder()
                .wireMock(wireMockRule)
                .build()
            SwagWiredPetApi api = new SwagWiredPetApi(configuration)
            SwagWiredPet pet = new SwagWiredPet()
                .name("Rover")
                .photoUrls(["http://url1", "http://url2"])
            api.getPetById(1234L).respondsWith(pet)

        expect:
            fetchingPet("1234").name == "Rover"
    }

Note that here our code is focused around domain objects instead of HTTP requests. Behind the scenes this has done exactly the same as the previous test, but with the safety that everything is done according to the openapi spec.

As of version 0.0.27, you can provide a WireMockServer instance to the configuration which allows for better test isolation when running as part of a complex build. This can typically just be the WireMockRule you're using.

Getting Started

Swagwire can be used the same way as any openapi code generation library. When setting up your codegen, ensure swagwire-codegen is on your classpath and specify the language as swagwire. A Gradle example is present in the examples directory. In addition, the swagwire-core jar is required to be on your test compile classpath. The provides the necessary libs and dependencies required for operation.

Swagwire will generate a Service API class for your service and corresponding data objects. For example, for the Pet store example you end up with the following files:

api/
   PetApi.java
   StoreApi.java
   UserApi.java
model/
   Category.java
   Order.java
   Pet.java
   Tag.java
   User.java

(plus supporting files)

All operations are created in the service API class, which can can create like this:

        String context = "/pet"
        SwagwiredServiceConfiguration config = SwagwiredServiceConfiguration.builder()
            .basePath(context)
            .wireMock(wireMockServer)
            .build()
        petApi = new PetApi(new ApiClient().setBasePath("${wireMockUrl()}/pet"))

This creates a Swagwire API for the Pet set of operations mounted at the /pet context.

Specify Requests

Requests are setup by calling the appropriate api method on your api class. These map directly to the operations specified in your openapi spec. All parameters will be automatically mapped to their correct location - path params, query params etc. When a body is provided this can either be a generated domain object or a ContentPattern<String>. The latter is provided for flexibility, for example only matching part of the body.

If you need to manipulate the request in a way Swagwire doesn't support directly, the RemoteOperation allows direct access to the current MappingBuilder.

Encoding Params

By default, Swagwire will not URL encode parameters that are part of the path. This can be enabled through configuration with a default encoder being provided that uses the JDK build in URLEncoder.

Setting up using the default URLEncoder:

SwagWiredServiceConfiguration configuration = SwagWiredServiceConfiguration.builder()
    .pathEncoder(new InterpolatingPathEncoder(UrlEncoderBasedUrlValueEncoder.INSTANCE))
    .build()

If this doesn't satisfy your needs you can provide your own PathEncoder. If you just want to provide a different mechanism for encoding the invidivual parts, you can use the InterpolatingPathEncoder and just provide your own implementation of UrlValueEncoder.

Response Configuration

When setting up an expectation you have to inform Swagwire how you want it to be handled, e.g.

    api.getPetById(1234L).respondsWith(pet)

If the API doesn't have a response object, you can inform Swagwire to return the correct code with no body:

    api.deletePet(1234L, "apiKey").succeeds()

All response operations are controlled via the RemoteOperation<T> object, where T is the type of response object, or Void where none is expected. From here you can setup standard responses, or if you want more control you can provide a ResponseDefinitionBuilder instance. For example, to simulate network problems:

    api.getPetById(1234L).respondsWith(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))

Note that if you fail to setup any expectations, your test will fail with Wiremock complaining that no stubs have been setup.

Multiple Responses

As of version 0.0.26 it is possible to setup multiple responses for a single request. This can be done by providing multiple reponses at once, as used in the examples:

    def "can respond multiple times"() {
        given:
            askingForPet().respondsWith(
                Data.of(rover),
                Response.ofStatus(404)
            )

        expect:
            fetchingPet().name == rover.name

        when:
            fetchingPet()

        then:
            thrown(ApiException)
    }

Interally this uses Wiremock scenarios, but hides the tediousness of setting them up from you.

Verification

If you want to use this to verify API calls happen after the fact, use the relevant operations on the RemoteOperation to validate what was called, like this:

    expect:
        askingForPet.wasCalled()

To setup calls in a lenient fashion, you can use wildcards for all input parameters, like this:

    given:
        remotePetApi.addPet(SwagwiredPet.ANY).succeeds()

    when:
        businessLogicIsExecuted()

    then:
        remotePetApi.addPet(SwagwiredPet.ANY).wasCalled()

For strings, the literal ".*" can be used.

Downloading

Swagwire is available from Maven Central.

Example Gradle

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        compile("com.masabi.swagwire:swagwire-codegen:1.0.10-openapi3")
    }
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("com.masabi.swagwire:swagwire-core:1.0.10-openapi3")
}

FAQs

Do I have to be using Swagger/openapi codegen in production to use this?

Not at all. The test bindings are completely isolated from your production code

Can I control JSON serialization/deserialization?

Yes. Your API constructor is accepts a configuration object which can include your own Gson instance in.

Do I have to use Spock/Groovy/Kotlin?

No, although you should because it's awesome. The bindings generated are 100% pure Java. The RemoteOperation class is written in Kotlin but it is 100% Java compatible.

How is date/time handled?

By default, the Java 8 date library type is used along with GsonJava8TypeAdapterFactory(). If you wish to change this, provide the necessary config in your codegen build and provide a relevantly configured Gson instance `

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published