Stubbing third party services

When you connect third party web services to your application, you'd like to have a convenient way to work with them during development.

1. The external service

Let's assume our application calls a REST API to validate user addresses.

GET https://imaginary-address-validation.com/api?address=Jungfernstieg%201,20095%20Hamburg

HTTP/1.1 200 OK
{valid:true}

2. The problem

In my current project, they've decided to choose a mechanism like this:

public boolean validate(Address address) {
    if (isOnLocalServer()) {
        return true;
    }

    return callRestApi(System.getProperty("ADDRESS_VALIDATION_API_ENDPOINT"), address);
}

This way, you can easily develop, while still having a working address validation on your production and testing systems, right?
Well, not really. The problem with this code snippet is: You can't reproduce an invalid address on your local system.

In order to test a failing address validation locally, you'd have to bloat this code to something like this:

public boolean validate(Address address) {
    if (isOnLocalServer()) {
        if ("invalidaddress".equals(address.getStreetName())) {
            return false;
        }
        return true;
    }

    return callRestApi(System.getProperty("ADDRESS_VALIDATION_API_ENDPOINT"), address);
}

So whenever you type in "invalidaddress" in the street name field, the validation will fail on your local development server. This can be handy for manual testing.

So what's the problem with this solution?
It is not automatically testable!

Yes, you can write a test which calls the method and tests the happy und unhappy path. But:

You'd like to avoid this, see below.

3. A better approach

My goal is to have:

To achieve this, you can write a very simple, lightweight stub, which just provides predefined responses for your requests. You start it in your local environment and the CI, and point the endpoint url to it (in this example ADDRESS_VALIDATION_API_ENDPOINT can point to e.g. http://localhost:8080/stubs/address-validation/api).
The stub can even be written using a different technology than your main application.
You can move the "invalidaddress" logic for successful and non-successful responses into the stub.

The production code will be cleaner, too, as it doesn't have to differentiate between your environment anymore:

public boolean validate(Address address) {
    return callRestApi(System.getProperty("ADDRESS_VALIDATION_API_ENDPOINT"), address);
}

Of course, you need to maintain the stub accordingly to the real API. However, to me that's not a real downside because you need to maintain the real API calls in your application anyway. This way, you even make sure your application works fine after an API has changed.

4. Example stub using kscript and ktor

For the simple API in this example, please find below a stub written in kscript and ktor. Ramp-up time is <2sec, and as you can see it's not much code and pretty straight-forward.
To run it, execute kscript stub.kscript, and it will start listening on port 8080. Example URL: http://0.0.0.0:8080/api?address=invalidaddress

@file:DependsOn("io.ktor:ktor-server-core:1.1.4")
@file:DependsOn("io.ktor:ktor-server-netty:1.1.4")
@file:DependsOn("ch.qos.logback:logback-classic:1.2.3")

import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.Logger.*
import ch.qos.logback.classic.Level.*
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import java.io.File
import org.slf4j.LoggerFactory

(LoggerFactory.getLogger(ROOT_LOGGER_NAME) as Logger).setLevel(INFO)

fun responseFilename(address: String): String {
    if (address == "invalidaddress") {
        return "failure.json"
    }
    return "success.json"
}

val server = embeddedServer(Netty, port = 8080) {
    routing {
        get("/api") {
            val address = call.request.queryParameters["address"]
            call.respondFile(File(responseFilename(address!!)))
        }
    }
}
server.start(wait = true)
{valid:true}
{valid:false}

5. Alternatives using mock servers

The approach above shows you the one with the greatest flexibiliy. In many cases, it is easier and faster to use static data.

My favorite mock server is: https://github.com/sinedied/smoke

For Java, I've used this in the past: https://www.mock-server.com