Using Spring Profiles to Statefully Mock Out Third Party Services in Docker

Topher Lamey
Topher Lamey
Share on twitter
Share on facebook
Share on linkedin
Share on reddit
Topher Lamey

Topher Lamey

Share on twitter
Share on facebook
Share on linkedin
Share on reddit

At StackHawk we are building services using Kotlin SpringBoot to deliver functionality to our platform. As part of building new functionality into these services, they end up needing to talk to various third-party services. As examples, we offer Slack and Jira integrations as user-level features and we also need the services to talk to things like SendGrid and Stripe. When developing and testing various pieces against the third-parties, we need fine-grain controls over communications and data. We’ve come up with several approaches that are detailed below.

Docker, Compose, and Helm

To help make our lives easier, we build Docker images of our services so we can use them in a number of ways: as dependencies when developing other services, in our CI/DI pipeline for automated integration testing, and then in actual test and production environments.

In general, we currently have two ways of running our services as Docker images:

  • In docker-compose as a dev, test, and CI/CD dependency
  • In helm in a Kubernetes pod as live services in test and production environments

Talking To or Mocking Out Third Parties

When interacting with third-party services, we often have intricacies to handle.  We’ll have environment requirements around whether or not our service should actually call out and, if not, whether or not we need it to be stateful. For example, when developing locally against the service when it’s running in docker-compose, we generally do not want it reaching out to any third parties ever. But we might need a way to emulate the actual service so it can return useful enough data for whatever’s needed to develop the feature.

ScannerService & Widget

We’ll walk through a couple solutions as examples.  For these examples, we’re going to have a ScannerService that has to integrate with a service called Widget using an API Key.

Special Values w/ Env Vars

Generally, for third party API Keys, we define an environment variable for Spring to read in via Spring’s application properties – in this case we’ll call it WIDGET_API_KEY. We would then set this on a per-environment basis, and we can overload it with a special “short circuit” value that can be used in code.

In the main application.yml:

widget:
 api-key: ${WIDGET_API_KEY:fake}

For docker-compose, which is mainly our dev/test/ci runtime, we don’t have to do anything because the ‘fake’ default value will be used and those will never call out.

But for running via helm in Kubernetes, we have a config yml per environment and would just simply override with the pointer to that env’s key in our secrets manager:

- name: WIDGET_API_KEY
 value: /reference/to/runtime/secrets/manager/widget/api/key

NOTE:  We don’t actually put api keys in yml files because it’s bad practice.  In reality, we use a runtime secrets manager that can do a key lookup against a secure vault, but that’s outside the scope of this article.

Finally in our code, we can do something like this:

class WidgetClient(@Value("\${widget.api-key}") widgetApiKey: String) {
    fun callWidget(payload: String) {
        if (!widgetApiKey.equals("fake")) {
            doActualCall(segmentUrl)
        }
    }
}

Explicit Flag w/Spring Profiles

Similarly to a special value, we can use an explicit flag in application.yml:

widget:
 enable: true

In this case, we used Spring profiles to set this value.  That way, the value is defined once but shared across anything that uses that profile.  That way we don’t have to remember all the places we’ve set that value, we can group these kinds of values in the profile, and it can be shared by whatever use the profile.

If we had a profile called ‘ci’, then we’d do something like this in application-ci.yml:

widget:
 enable: true

If we need that profile to run in docker-compose, we set it in the ScannerService config like so:

version: "3"
services:
 screech:
   image: stackhawk/scanner:latest
   container_name: scanner
   ports:
     - 4400
   environment:
     - SPRING_PROFILES_ACTIVE=ci

For helm, since the default is true, it will make the third party calls.  But if we needed a specific profile, it’d look like this:

common:
 env:
   - name: SPRING_PROFILES_ACTIVE
     value: runtime

Our code then becomes a little clearer in that we have a single, well-defined and understood flag rather than overloading something else with the magic value:

class WidgetClient(@Value("\${widget.disabled}") isWidgetDisabled: String) {
    fun callWidget(payload: String) {
        if (!isWidgetDisabled) {
            doActualCall(segmentUrl)
        }
    }
}

Problem Solved?

Using special overloaded values or explicit flags solves the problem, but adds complexity to the code, which makes it more difficult in general to add functionality and also to track down runtime issues.  Another downside we ran into was when someone added more calls against Widget, they didn’t know that they needed to take the special value/explicit flag into consideration.  In the examples above it’s trivial to see what’s going on, but in a bigger class with more going on, it wasn’t clear.  In that case, the overall system behavior became difficult to understand because that service was making some calls to the third party but not all. 

Stateful Testing

Up until recently, our third party integrations just needed to be ‘off’ or ‘on’ for a given environment and the above approaches worked.  Then we had to add some new functionality that required a service to be stateful but not actually hit the third party when running in certain test and dev modes.  In order to completely develop, test, and deliver the solution across our platform, the service needed to persist and return data but not actually reach out to the third party service in some environments.

The third party in question here had a Dockerized mock service that seemed at first to solve our issue.  But unfortunately, that mock service wasn’t stateful and a lot of things weren’t mutable – it was unable to set certain states because the real service didn’t allow it…but we needed to dev and test with those statuses.

Profile Based Mock Implementation

Luckily, we already had the pieces needed to provide a solution in that we were using Spring Profiles in both our docker-compose and helm strategies.  The strategy was to use Spring profiles to provide either the real client or a statefully mocked one.  This gave us two benefits:  it completely separates out the two codepaths between ‘live’ and ‘mock’ mode, and also gives ‘mock’ mode the ability to keep state where needed.

Here’s what that might look like in an interface and two implementing classes that use the Spring @Profile annotation to specify which one will be used:

interface WidgetClient {
   fun createWidget(params: CreateWidgetParams): Widget
}
@Profile("!ci && !dev")
@Component
class WidgetClientImpl(@Value("\${wideget.api-key}") apiKey: String) : WidgetClient {
    fun createWidget(params: CreateWidgetParams): Widget {
        val widget = makeTheActualCallToWidget(params)
        processAndSave(widget)
        return widget
    }
}

@Profile("ci | dev")
@Component
class WidgetClientMock: WidgetClient {

    val name = "test-name"

    fun createWidget(params: CreateWidgetParams): Widget {
        val widget = Widget(params)
        widget.name = name
        processAndSave(widget)
        return widget
    }
}

The WidgetClientImpl is blissfully unaware of any sort of mock/test codepaths and just assumes it’s doing the real thing.  The WidgetClientMock then doesn’t know anything about what’s supposed to happen and just focuses on local, mocked data.  Both are easier to understand and much easier to test.

If something isn’t configured correctly and the wrong environment gets the wrong WidgetClient implementation, it becomes quickly apparent that something’s wrong.  For example, since the profiles that do mocking don’t have a real key, if they get the WidgetClientImpl and start making calls against Widget, it will fail early and loudly.  If the opposite happens and an env gets the mock implementation, it’s obvious that nothing is actually calling out to Widget.

Summary

Having to work with third-party services is commonly requested functionality these days, and having multiple options is key. Using a single overloaded value or an explicit flag with conditional checks keeps your logic contained in a single service, but makes that service more complicated.  Letting the runtime pick an implementation based on a runtime value like a Spring Profile lets you split the test code from an actual implementation, allows a mock implementation to have a test state, but adds more configuration to your application.

More StackHawk
Ryan Severns
Zachary Conger
Scott Gerlach

KAAKAWW!!! [ kǝn'grats ]

The Demo Gods Approve!
We’ll reach out to you soon to schedule a 45 minute demo. Please complete this 3 minute survey so we can prepare a demo that is specific to you.

KAAKAWW!!! [ kǝn'grats ]

You're signed up for the newsletter!
We’ll keep you up to date on content and other happenings here at StackHawk.