days
0
-35
-6
hours
-1
-5
minutes
0
0
seconds
-3
-1
search
Jobrunr tutorial

A hands-on tutorial on how to test against 12 different JVM’s using TestContainers

Ronald Dehuysser
JVM
© Shutterstock / PowerUp

JobRunr is an open source library that makes it easy to perform background processing on the JVM, making use of persistent storage like an RDBMS or a NoSQL database. This tutorial will show you how to test against 12 different JVMs using TestContainers.

  • 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.

SEE ALSO: 7 JVM arguments of highly effective applications

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;
    }
}

The test enqueues a job and then queries the rest API to see whether the job succeeded.

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

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, a Docker image is build on the fly using a base image that can be passed along. 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 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
// the end result is that only the tests inside org.jobrunr.tests.e2e must run (on the correct JDK) and not this test
@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 build is done which 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!

SEE ALSO: JVMs in Containers, Towards a Perfect Symbiosis!

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!

Author

Ronald Dehuysser

Ronald Dehuysser, is a freelance coach and software architect/developer pondering on how IT can make the world greener. He maintains JobRunr, an extremely easy library to perform distributed background processing on the JVM and also makes some of the tastiest pizza’s!


Leave a Reply

avatar
400