Testing a Spring Boot application with the Google Cloud Spanner Emulator
Cloud Spanner has been around for a while, but testing your applications against a real instance could be difficult:
- There is a cost involved using a live instance for testing — “Don’t test too much it’s costing us money!”
- You have to be online and have decent connectivity — not great if you’re working while commuting
Recently, Google released the Cloud Spanner Emulator. The emulator can run on your own machine and is compatible with a real Spanner instance — it can run exactly the same SQL statements as the real Spanner.
In this guide, we’ll go through how to set up the emulator to run as part of a build of a small Spring Boot Java application.
Using the emulator
The Cloud Spanner emulator can run as a normal process under Linux, or with Docker on other platforms. We’ll be using the docker image in this guide since it’s easy and self-contained, and I like my builds to run anywhere.
The emulator project is here on Github. There are instructions on how to get this working under Docker here, but let’s put all this logic in our build so that other devs (and the CI server) won’t have to worry about it.
A simple Spring Boot application
Our application will be very simple, using Spring Data for data access. A couple of entities for the demo:
and their repositories:
Test Configuration
For integration testing, a bit of setup is required. This piece of Spring configuration will be used for setting up the testing environment:
We will be using a different Spanner configuration for testing, reading project, instance and database IDs from system properties, using defaults if they are not specified. This will allow users to hit a real Spanner instance if they want to run tests manually, or on special CI builds. Maybe we want to use the emulator for day-to-day CI but for builds before releases we’ll use a real Spanner instance?
The environment variable SPANNER_EMULATOR_HOST will be configured with the location of the Spanner emulator. If not specified, an attempt will be made to use a real Spanner instance instead.
When the Spanner emulator is started, there is a single project available with no instances configured. This code:
finds the only instance config available in the emulator and also finds the only available project. From this information, a new instance can be created, and then a new database. When the database ID provider bean is created (which is only once for the application), this is a good point to set all of this up. It’s pretty cheap to do anyway in the emulator, so even if you decide to do this for every test method it shouldn’t add too much overhead.
The Integration Tests
Now we have all of this set up, we can write a test:
We’ll use the repository classes to delete all the data and recreate it between each test, so we’re always testing in a known state. Then exercise a couple of the repositories just to prove the emulator is working.
Wiring everything together with a Maven project
We will start with a pretty simple Spring Boot project with Spanner dependencies:
So far, we have nothing to run the integration tests. Next, we’ll add a Docker plugin that will spin up a Docker container to run the Spanner emulator:
The first part is specifying the Docker image. The name of the Docker image is gcr.io/cloud-spanner-emulator/emulator:1.0.0 which we get from the Spanner emulator documentation. Since we want a consistent build, we’ll pick a concrete version number to use, 1.0.0. The list of available versions can be determined by listing the tags with the gcloud command line tools:
gcloud container images list-tags
gcr.io/cloud-spanner-emulator/emulator
The emulator exposes a TCP port which we’ll use. The configuration:
<port>+spanner.emulator.host:spanner.emulator.port:9010</port>
will map port 9010 in the container to a randomly selected free port on the host, and will also save a couple of system properties containing the host port and the Docker host itself. Usually the Docker host will be localhost but now always — it depends on your platform — for example users that use docker-machine for launching Docker will have a host corresponding to a virtual machine.
Below that, we instruct the Docker plugin to not continue with the build until the TCP port 9010 is responding — we don’t want to kick off our tests before the emulator has fully started up.
The remaining part is to configure the execution of the integration tests themselves using the failsafe plugin:
The SPANNER_EMULATOR_HOST environment variable is being configured with the host and port of the emulator that was spun up by Docker. The rest is stock-standard integration test configuration for failsafe.
Run mvn verify and the Spanner emulator will be started in Docker, then the tests will run, and finally the emulator container is stopped and removed again. The first time this is run you’ll need network connectivity to download dependencies and the Spanner Docker image, but after that the build can be run even if you don’t have any network/Internet access.
Using a real Spanner instance
If there is a need to run tests against a real Spanner instance, we can change the project a bit to allow this.
Let’s define 3 build properties that could potentially be configured from the command line: test.projectId, test.instanceId and test.databaseId. The JUnit test code previously written already accommodates these, so let’s thread these through in the build file by redefining the failsafe plugin configuration:
But what about the emulator? Let’s move all the emulator code into a Maven profile:
Here we have a profile that will be always activated unless test.projectId is configured, so it will work like before. The SPANNER_EMULATOR_HOST environment variable will only be set in failsafe if this profile is active.
So now, without any properties set, the emulator will run and the build will run the tests with the emulator. But if there is a need to run against a real Spanner instance, set those three properties:
mvn verify -Dtest.projectId=helix-sydney -Dtest.instanceId=myinstance -Dtest.databaseId=cats
You’ll need to configure credentials for your instance before you run.