How I test JobRunr against 12 different JVM’s

Ronald Dehuysser
4 min readJun 1, 2020

--

JobRunr, a library which utilizes Java 8 lambdas to schedule fire-and-forget, delayed and recurring jobs, analyses the generated bytecode of your Java application to find out which lambda you want to run in the background. As it allows to process background jobs in a distributed manner, it makes use of persistent storage like an RDBMS or a NoSQL database.

Today, there are several vendors of Java virtual machines — there is OpenJDK, Oracle JDK, GraalVM by Oracle, Adopt-OpenJ9 and Zulu among others.

And then there are all kinds of databases like Postgres, Oracle XE, Microsoft SQL Server MySql and MariaDB — not even mentioning the NoSQL stores like MongoDB and Redis.

Since JobRunr uses bytecode analysis to perform it’s job (pun intended), I thought it was important to have a test where different job lambda’s are compiled and executed on each different JVM instance.

To do so, I first created the following test which has unit tests with different ways of enqueueing background jobs using JobRunr:

package org.jobrunr.tests.e2e;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.jobs.lambdas.JobLambda;
import org.jobrunr.scheduling.BackgroundJob;
import org.jobrunr.storage.SimpleStorageProvider;
import org.jobrunr.tests.e2e.services.TestService;
import org.jobrunr.utils.mapper.gson.GsonJsonMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.awaitility.Awaitility.await;
import static org.jobrunr.tests.fromhost.HttpClient.getJson;

public class E2EJDKTest {

private TestService testService;

@BeforeEach
public void startJobRunr() {
testService = new TestService();

JobRunr
.configure()
.useStorageProvider(new SimpleStorageProvider().withJsonMapper(new GsonJsonMapper()))
.useJobActivator(this::jobActivator)
.useDashboard()
.useDefaultBackgroundJobServer()
.initialize();
}

@AfterEach
public void stopJobRunr() {
JobRunr
.destroy();
}

@Test
void usingLambdaWithIoCLookupUsingInstance() {
BackgroundJob.enqueue(() -> testService.doWork(UUID.randomUUID()));

await()
.atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> assertThatJson(getSucceededJobs()).inPath("$.items[0].jobHistory[2].state").asString().contains("SUCCEEDED"));
}

@Test
void usingLambdaWithIoCLookupWithoutInstance() {
BackgroundJob.<TestService>enqueue(x -> x.doWork(UUID.randomUUID()));

await()
.atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> assertThatJson(getSucceededJobs()).inPath("$.items[0].jobHistory[2].state").asString().contains("SUCCEEDED"));
}

@Test
void usingMethodReference() {
BackgroundJob.enqueue((JobLambda)testService::doWork);

await()
.atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> assertThatJson(getSucceededJobs()).inPath("$.items[0].jobHistory[2].state").asString().contains("SUCCEEDED"));
}

@Test
void usingMethodReferenceWithoutInstance() {
BackgroundJob.<TestService>enqueue(TestService::doWork);

await()
.atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> assertThatJson(getSucceededJobs()).inPath("$.items[0].jobHistory[2].state").asString().contains("SUCCEEDED"));
}

private String getSucceededJobs() {
return getJson("http://localhost:8000/api/jobs/default/succeeded");
}

private <T> T jobActivator(Class<T> clazz) {
return (T) testService;
}
}

Here, we enqueue some jobs and confirm that the job was executed successfully using the REST api of JobRunr.

To then run this test against 12 different JVM’s, I used TestContainers:

package org.jobrunr.tests.fromhost;

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.utility.MountableFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

import static java.nio.file.Files.exists;

public class BuildAndTestContainer extends GenericContainer<BuildAndTestContainer> {

public BuildAndTestContainer(String fromDockerImage) {
super(new ImageFromDockerfile()
.withDockerfileFromBuilder(builder ->
builder
.from(fromDockerImage)
.workDir("/app/jobrunr")
.env("JDK_TEST", "true")
));
if (exists(Paths.get("/drone"))) {
this
.withFileSystemBind(Paths.get("/tmp/jobrunr/cache/gradle-wrapper").toString(), "/root/.gradle/wrapper/dists");
} else {
this
.withFileSystemBind(Paths.get(System.getProperty("user.home"), ".gradle", "wrapper", "dists").toString(), "/root/.gradle/wrapper/dists");
}

this
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(".")), "/app/jobrunr")
.withCommand("./gradlew", "build")
.waitingFor(Wait.forLogMessage(".*BUILD SUCCESSFUL.*", 1));
}
}

Here, a Docker image is build on the fly using a base image that can be passed along in the constructor. A special environment variable called JDK_TEST is added and the gradle wrapper is mounted inside the docker image (which differs on my local system and the CI server). Next, the current gradle module is copied into the container and the ./gradlew build command is run. And finally, the container waits for the log message 'BUILD SUCCESFUL' as it would otherwise stop immediately.

To run the E2EJDKTest within the different JVM instances, the following test is used:

package org.jobrunr.tests.fromhost;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
import org.junit.jupiter.executioncondition.RunTestBetween;

import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;

// why: we create a build of the current gradle module inside docker container for each JDK
// we do not want to run this test within the docker container itself as it would otherwise run recursively
// once inside the docker build, the ENV variable JDK_TEST is set, making sure this test is not run again
// the end result is that only the tests inside org.jobrunr.tests.e2e must run (on the correct JDK)
@DisabledIfEnvironmentVariable(named = "JDK_TEST", matches = "true")
public class JdkTest {

@Test
public void jdk8OpenJdk() {
assertThat(buildAndTestOnImage("adoptopenjdk:8-jdk-hotspot")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk8OpenJ9() {
assertThat(buildAndTestOnImage("adoptopenjdk:8-jdk-openj9")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk8Zulu() {
assertThat(buildAndTestOnImage("azul/zulu-openjdk:8")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk8GraalVM() {
assertThat(buildAndTestOnImage("oracle/graalvm-ce:20.1.0-java8")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk8Ibm() {
assertThat(buildAndTestOnImage("ibmcom/ibmjava:8-sdk-alpine")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk11OpenJdk() {
assertThat(buildAndTestOnImage("adoptopenjdk:11-jdk-hotspot")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk11OpenJ9() {
assertThat(buildAndTestOnImage("adoptopenjdk:11-jdk-openj9")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk11Zulu() {
assertThat(buildAndTestOnImage("azul/zulu-openjdk:11")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk11GraalVM() {
assertThat(buildAndTestOnImage("oracle/graalvm-ce:20.1.0-java11")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk14OpenJdk() {
assertThat(buildAndTestOnImage("adoptopenjdk:14-jdk-hotspot")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk14OpenJ9() {
assertThat(buildAndTestOnImage("adoptopenjdk:14-jdk-openj9")).contains("BUILD SUCCESSFUL");
}

@Test
public void jdk14Zulu() {
assertThat(buildAndTestOnImage("azul/zulu-openjdk:14")).contains("BUILD SUCCESSFUL");
}

private String buildAndTestOnImage(String dockerfile) {
final BuildAndTestContainer buildAndTestContainer = new BuildAndTestContainer(dockerfile);
buildAndTestContainer
.withStartupTimeout(Duration.ofMinutes(10))
.start();
return buildAndTestContainer.getLogs();
}
}

So, for each different JVM, the source code is copied inside that JVM and then a gradle build is done which also runs the E2EJDKTest. This makes sure that the code is compiled and the tests are executed within that JVM. To confirm that all is well, the logs of the container are requested and using the excellent AssertJ library, an assertion is done to make sure the container logs contain 'BUILD SUCCESSFUL'.

And that’s it!

Learn more

I hope you enjoyed this tutorial and you can see that for JobRun quality and testing is taken seriously.

To learn more, check out these guides:

If you liked this tutorial, feel free to star us on GitHub!

Editorial update

I initially thought that TestContainer was developped by Google but this is not correct. It is developped by Richard North and other authors.

--

--

Ronald Dehuysser

All-round software gardener (Java, .NET core, Javascript, TypeScript, Python) and visual facilitator. You can reach me via linkedin.