Never miss one of my articles: Readers of my newsletter get my articles before anyone else. Subscribe here!

In the last week or so, I was playing a little bit with microservices (hey! a buzzword!), and I used Spring Boot to create those services. On of my first questions was: How can I test a set of services from a business point of view with a single click in my IDE - I.e. how can I ensure that the complete application has the right features? I wanted a way to start multiple Spring Boot web applications in the same JVM. Here is how I did it.

The Problem

I wanted to create an application that handles location data. It would consist of three services: One to write locations (“locations-command”, one to read them (“locations-query”) and a web application. You see, we are doing CQRS here (another buzzword!).

The web application contains only Spring WebMVC controllers and ViewModels (add MVVM to the list of buzzwords…). It calls the locations-query or locations-command service to do the real work. Those services would then use some storage backend to store and retrieve locations - Probably couchbase, but I have not decided yet.

I want to test this application from a business point of view using “executable specifications” written in FitNesse. I want to run those tests either in FitNesse or with JUnit from within my IDE. But I do not want to build and run a set of docker containers every time - I want to run the tests all in the same IDE, so I start them with a single click and debug them if necessary.

I also don’t want those tests to use the real storage backend. I want to be able to mock backend calls, and only test against the real database in a separate set of tests (which would then really spin up all those docker containers). I have not completely solved this part yet, so I’ll not cover it here. Maybe in a later blog post…

Third, I want that all the services to use port 8080 when they run in their own docker container - I don’t want to customize ports within the application. I can do this later with docker. But when I run the services within the same JVM, they have to use different ports.

And fourth, all spring boot applications have to run completely independent from each other - They cannot share the classpath or anything else.

The Setup

My project structure roughly looks like this:

microservices-test/
    |-webapp/
    |-locations-command/
    |-locations-query/
    |-specifications/
    |-servicerunners/
    |    |-backend-runner/
    |    |-locations-command/
    |    \-locations-query/
    |-build.gradle
    \-settings.gradle

All the subprojects contain their own build.gradle, source folders, and other stuff.

Here is the global settings.gradle and the global build.gradle:

/settings.gradle

include 'webapp', ':locations-query', ':locations-command',
    ':specifications',
    ':servicerunners:backend-runner', 
    ':servicerunners:locations-query', ':servicerunners:locations-command'

/build.gradle

buildscript {
	ext {
		springBootVersion = '1.2.2.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'java'
apply plugin: 'spring-boot'

allprojects {
	sourceCompatibility = 1.8
	targetCompatibility = 1.8

	repositories {
		mavenCentral()
	}
}

subprojects {
	buildscript {
		ext {
			springBootVersion = '1.2.2.RELEASE'
		}
		repositories {
			mavenCentral()
		}
		dependencies {
			classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
			classpath 'se.transmode.gradle:gradle-docker:1.2'
		}
	}
	apply plugin: 'java'
	apply plugin: 'spring-boot'

	dependencies {
		testCompile 'junit:junit:4.12'
	}

	task wrapper(type: Wrapper) {
		gradleVersion = '2.3'
	}
}

“webapp”, “locations-command” and “locations-query” in the project root are the services. Those are standard Spring Boot web applications - You can create them, for example, with this web application: start.spring.io.

“specifications” contains the test fixtures and support code for the FitNesse tests. The three subprojects under “servicerunners” contain the code to start the web applications we need for testing.

For the tests we will only start the two locations services as spring boot applications. We will drive the tests through the ViewModels of the web application, so we don’t need to start the Jetty of “webapp”.

Test Suite Setup and Teardown

The specifications project must have the “webapp” in its classpath (so we can drive the tests through the ViewModels), but it cannot reference any other projects directly. Otherwise the two services would share the same classpath, and we don’t want that. It also has to reference the required libraries from Spring Boot, so they are available for the service runners (see later).

/specifications/build.gradle

dependencies {
	compile project(':webapp')

	compile("org.springframework.boot:spring-boot-starter-jersey")
	compile("org.springframework.boot:spring-boot-starter-web")

	testCompile 'org.fitnesse:fitnesse:20150226'
}

The FitnessSuiteHelper is where the magic happens. This class starts each service using a dedicated “BackendRunner” - See below what it does and why we need it.

/specifications/src/main/java/…/FitnesseSuiteHelper.java

public class FitnesseSuiteHelper {
    private static final List<Backend> activeBackends = new ArrayList<>();

    public FitnesseSuiteHelper() {
    }

    public static void startBackends() throws Exception {
        startBackend("locations-query", "com.example.LocationsQueryBackendRunner");
        startBackend("locations-command", "com.example.LocationsCommandBackendRunner");
    }

    private static void startBackend(final String backendProjectName, 
            final String backendClassName) throws Exception {
        URL backendRunnerUrl = new File("servicerunners/backend-runner/build/classes/main")
            .toURI().toURL();
        URL runnerUrl = new File("servicerunners/" + backendProjectName 
            + "/build/classes/main").toURI().toURL();
        URL backendUrl = new File(backendProjectName 
            + "/build/classes/main").toURI().toURL();
        URL[] urls = new URL[] { backendUrl, backendRunnerUrl, runnerUrl };
        URLClassLoader cl = new URLClassLoader(urls, 
            FitnesseSuiteHelper.class.getClassLoader());
        Class<?> runnerClass = cl.loadClass(backendClassName);
        Object runnerInstance = runnerClass.newInstance();

        final Backend backend = new Backend(runnerClass, runnerInstance);
        activeBackends.add(backend);

        runnerClass.getMethod("run").invoke(runnerInstance);
    }

    public static void stopAllBackends() 
            throws IllegalAccessException, InvocationTargetException, 
            NoSuchMethodException {
        for(Backend b : activeBackends) {
            b.runnerClass.getMethod("stop").invoke(b.runnerInstance);
        }
    }

    private static class Backend {
        private Class<?> runnerClass;
        private Object runnerInstance;

        public Backend(final Class<?> runnerClass, 
                       final Object runnerInstance) {
            this.runnerClass = runnerClass;
            this.runnerInstance = runnerInstance;
        }
    }
}

The “startBackend” method starts each service in its own classloader using a “BackendRunner”. To do this, it has to configure the correct classpath: We need the service itself (“backendUrl”), the backend runner for this specific service (“runnerUrl”) and the generic backend runner (“backendRunnerUrl”). We also have to keep a reference to all backends, so we can stop them after the test suite has finished (“stopAllBackends()”).

The “BackendRunner” for each service is very simple: It only contains a constructor that configures the generic BackendRunner.

/servicerunners/locations-query/…/LocationsQueryBackendRunner.java

public class LocationsQueryBackendRunner extends BackendRunner {
    public LocationsQueryBackendRunner() {
        super(LocationsQueryBackendApplication.class, CustomizationBean.class);
    }
}

The CustomizationBean makes sure that each web application runs on a different port. We could add other beans in the constructor to further customize the service for testing.

/servicerunners/locations-query/…/CustomizationBean.java

@Component
public class CustomizationBean implements EmbeddedServletContainerCustomizer {
    @Override
    public void customize(ConfigurableEmbeddedServletContainer container) {
        container.setPort(BackendPorts.LOCATIONS_QUERY_BACKEND_PORT);
    }
}

And the generic BackendRunner does the real work:

/servicerunners/backend-runner/…/BackendRunner.java

public abstract class BackendRunner {
    private ConfigurableApplicationContext appContext;
    private final Class<?>[] backendClasses;

    private Object monitor = new Object();
    private boolean shouldWait;

    protected BackendRunner(final Class<?>... backendClasses) {
        this.backendClasses = backendClasses;
    }

    public void run() {
        if(appContext != null) {
            throw new IllegalStateException("AppContext must be null to run this backend");
        }
        runBackendInThread();
        waitUntilBackendIsStarted();
    }

    private void waitUntilBackendIsStarted() {
        try {
            synchronized (monitor) {
                if(shouldWait) {
                    monitor.wait();
                }
            }
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }

    private void runBackendInThread() {
        final Thread runnerThread = new BackendRunnerThread();
        shouldWait = true;
        runnerThread.setContextClassLoader(backendClasses[0].getClassLoader());
        runnerThread.start();
    }

    public void stop() {
        SpringApplication.exit(appContext);
        appContext = null;
    }

    private class BackendRunnerThread extends Thread {
        @Override
        public void run() {
            appContext = SpringApplication.run(backendClasses, new String[]{});
            synchronized (monitor) {
                shouldWait = false;
                monitor.notify();
            }
        }
    }
}

Actually this class only has to call appContext = SpringApplication.run(backendClasses, new String[]{}). But it has to do so on a new thread with the correct contextClassLoader, otherwise Spring would not pick up the correct classloader.

So we have to run the Spring Boot applicatoin in its own thread (“BackendRunnerThread”) and also wait until it finished starting up. We do this by waiting on a monitor in “waitUntilBackendIsStarted()” - The runner thread will call “monitor.notify()” when the application is started.

To Recap

We can start multiple Spring Boot web applications when

  • They all run in their own classloader.
  • They run in their own thread, so Spring can use the contextClassLoader of the thread
  • They use different ports when running in the same VM
  • The application under test (who calls the services) and the test fixtures do not have a direct reference to them.

The code to support this is actually surprisingly simple (at least IMHO), but you’ll need some extra subprojects to configure the classpath correctly.

Do you have any questions or comments about this article? Please tell me!

Do you want to get articles like this on a regular basis, in your mail? Subscribe here!

You might be also interested in: