Skip to content

Neo4j Module

This module helps running Neo4j using Testcontainers.

Note that it's based on the official Docker image provided by Neo4j, Inc.

Usage example

Declare your Testcontainers as a @ClassRule or @Rule in a JUnit 4 test or as static or member attribute of a JUnit 5 test annotated with @Container as you would with other Testcontainers. You can either use call getBoltUrl() or getHttpUrl() on the Neo4j container. getBoltUrl() is meant to be used with one of the official Bolt drivers while getHttpUrl() gives you the HTTP-address of the transactional HTTP endpoint. On the JVM you would most likely use the Java driver.

The following example uses the JUnit 5 extension @Testcontainers and demonstrates both the usage of the Java Driver and the REST endpoint:

@Testcontainers
class Neo4jExampleTest {

    @Container
    private static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>(DockerImageName.parse("neo4j:4.4"))
        .withoutAuthentication(); // Disable password

    @Test
    void testSomethingUsingBolt() {
        // Retrieve the Bolt URL from the container
        String boltUrl = neo4jContainer.getBoltUrl();
        try (Driver driver = GraphDatabase.driver(boltUrl, AuthTokens.none()); Session session = driver.session()) {
            long one = session.run("RETURN 1", Collections.emptyMap()).next().get(0).asLong();
            assertThat(one).isEqualTo(1L);
        } catch (Exception e) {
            fail(e.getMessage());
        }
    }

    @Test
    void testSomethingUsingHttp() throws IOException {
        // Retrieve the HTTP URL from the container
        String httpUrl = neo4jContainer.getHttpUrl();

        URL url = new URL(httpUrl + "/db/data/transaction/commit");
        HttpURLConnection con = (HttpURLConnection) url.openConnection();

        con.setRequestMethod("POST");
        con.setRequestProperty("Content-Type", "application/json");
        con.setDoOutput(true);

        try (Writer out = new OutputStreamWriter(con.getOutputStream())) {
            out.write("{\"statements\":[{\"statement\":\"RETURN 1\"}]}");
            out.flush();
        }

        assertThat(con.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK);
        try (BufferedReader buffer = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
            String expectedResponse =
                "{\"results\":[{\"columns\":[\"1\"],\"data\":[{\"row\":[1],\"meta\":[null]}]}],\"errors\":[]}";
            String response = buffer.lines().collect(Collectors.joining("\n"));
            assertThat(response).isEqualTo(expectedResponse);
        }
    }
}

You are not limited to Unit tests and can of course use an instance of the Neo4j Testcontainers in vanilla Java code as well.

Additional features

Custom password

A custom password can be provided:

package org.testcontainers.containers;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import org.junit.Test;
import org.neo4j.driver.AuthToken;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Record;
import org.neo4j.driver.Result;
import org.neo4j.driver.Session;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
import org.testcontainers.utility.DockerLoggerFactory;
import org.testcontainers.utility.MountableFile;

import java.util.Collections;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assumptions.assumeThat;

/**
 * Tests of functionality special to the Neo4jContainer.
 */
public class Neo4jContainerTest {

    // See org.testcontainers.utility.LicenseAcceptance#ACCEPTANCE_FILE_NAME
    private static final String ACCEPTANCE_FILE_LOCATION = "/container-license-acceptance.txt";

    @Test
    public void shouldDisableAuthentication() {
        try (
            // spotless:off
            // withoutAuthentication {
            Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
                .withoutAuthentication()
            // }
            // spotless:on
        ) {
            neo4jContainer.start();
            try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) {
                long one = session.run("RETURN 1", Collections.emptyMap()).next().get(0).asLong();
                assertThat(one).isEqualTo(1L);
            }
        }
    }

    @Test
    public void shouldCopyDatabase() {
        // no aarch64 image available for Neo4j 3.5
        assumeThat(DockerClientFactory.instance().getInfo().getArchitecture()).isNotEqualTo("aarch64");
        try (
            // copyDatabase {
            Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:3.5.30")
                .withDatabase(MountableFile.forClasspathResource("/test-graph.db"))
            // }
        ) {
            neo4jContainer.start();
            try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) {
                Result result = session.run("MATCH (t:Thing) RETURN t");
                assertThat(result.list().stream().map(r -> r.get("t").get("name").asString()))
                    .containsExactlyInAnyOrder("Thing", "Thing 2", "Thing 3", "A box");
            }
        }
    }

    @Test
    public void shouldFailOnCopyDatabaseForDefaultNeo4j4Image() {
        assertThatIllegalArgumentException()
            .isThrownBy(() -> new Neo4jContainer<>().withDatabase(MountableFile.forClasspathResource("/test-graph.db")))
            .withMessage("Copying database folder is not supported for Neo4j instances with version 4.0 or higher.");
    }

    @Test
    public void shouldFailOnCopyDatabaseForCustomNeo4j4Image() {
        assertThatIllegalArgumentException()
            .isThrownBy(() -> {
                new Neo4jContainer<>("neo4j:4.4.1").withDatabase(MountableFile.forClasspathResource("/test-graph.db"));
            })
            .withMessage("Copying database folder is not supported for Neo4j instances with version 4.0 or higher.");
    }

    @Test
    public void shouldFailOnCopyDatabaseForCustomNonSemverNeo4j4Image() {
        assertThatIllegalArgumentException()
            .isThrownBy(() -> {
                new Neo4jContainer<>("neo4j:latest").withDatabase(MountableFile.forClasspathResource("/test-graph.db"));
            })
            .withMessage("Copying database folder is not supported for Neo4j instances with version 4.0 or higher.");
    }

    @Test
    public void shouldCopyPlugins() {
        try (
            // registerPluginsPath {
            Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
                .withPlugins(MountableFile.forClasspathResource("/custom-plugins"))
            // }
        ) {
            neo4jContainer.start();
            try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) {
                assertThatCustomPluginWasCopied(session);
            }
        }
    }

    @Test
    public void shouldCopyPlugin() {
        try (
            // registerPluginsJar {
            Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
                .withPlugins(MountableFile.forClasspathResource("/custom-plugins/hello-world.jar"))
            // }
        ) {
            neo4jContainer.start();
            try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) {
                assertThatCustomPluginWasCopied(session);
            }
        }
    }

    private static void assertThatCustomPluginWasCopied(Session session) {
        Result result = session.run("RETURN ac.simons.helloWorld('Testcontainers') AS greeting");
        Record singleRecord = result.single();
        assertThat(singleRecord).isNotNull();
        assertThat(singleRecord.get("greeting").asString()).isEqualTo("Hello, Testcontainers");
    }

    @Test
    public void shouldCheckEnterpriseLicense() {
        assumeThat(Neo4jContainerTest.class.getResource(ACCEPTANCE_FILE_LOCATION)).isNull();

        String expectedImageName = "neo4j:4.4-enterprise";

        assertThatExceptionOfType(IllegalStateException.class)
            .isThrownBy(() -> new Neo4jContainer<>("neo4j:4.4").withEnterpriseEdition())
            .withMessageContaining("The image " + expectedImageName + " requires you to accept a license agreement.");
    }

    @Test
    public void shouldRunEnterprise() {
        assumeThat(Neo4jContainerTest.class.getResource(ACCEPTANCE_FILE_LOCATION)).isNotNull();

        try (
            // enterpriseEdition {
            Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
                .withEnterpriseEdition()
                // }
                .withAdminPassword("Picard123")
        ) {
            neo4jContainer.start();
            try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) {
                String edition = session
                    .run("CALL dbms.components() YIELD edition RETURN edition", Collections.emptyMap())
                    .next()
                    .get(0)
                    .asString();
                assertThat(edition).isEqualTo("enterprise");
            }
        }
    }

    @Test
    public void shouldAddConfigToEnvironment() {
        // neo4jConfiguration {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
            .withNeo4jConfig("dbms.security.procedures.unrestricted", "apoc.*,algo.*")
            .withNeo4jConfig("dbms.tx_log.rotation.size", "42M");
        // }

        assertThat(neo4jContainer.getEnvMap())
            .containsEntry("NEO4J_dbms_security_procedures_unrestricted", "apoc.*,algo.*");
        assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_dbms_tx__log_rotation_size", "42M");
    }

    @Test
    public void shouldRespectEnvironmentAuth() {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withEnv("NEO4J_AUTH", "neo4j/secret");

        neo4jContainer.configure();

        assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/secret");
    }

    @Test
    public void shouldSetCustomPasswordCorrectly() {
        // withoutAuthentication {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withAdminPassword("verySecret");
        // }

        neo4jContainer.configure();

        assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/verySecret");
    }

    @Test
    public void containerAdminPasswordOverrulesEnvironmentAuth() {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
            .withEnv("NEO4J_AUTH", "neo4j/secret")
            .withAdminPassword("anotherSecret");

        neo4jContainer.configure();

        assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/anotherSecret");
    }

    @Test
    public void containerWithoutAuthenticationOverrulesEnvironmentAuth() {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
            .withEnv("NEO4J_AUTH", "neo4j/secret")
            .withoutAuthentication();

        neo4jContainer.configure();

        assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_AUTH", "none");
    }

    @Test
    public void shouldRespectAlreadyDefinedPortMappingsBolt() {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withExposedPorts(7687);

        neo4jContainer.configure();

        assertThat(neo4jContainer.getExposedPorts()).containsExactly(7687);
    }

    @Test
    public void shouldRespectAlreadyDefinedPortMappingsHttp() {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withExposedPorts(7474);

        neo4jContainer.configure();

        assertThat(neo4jContainer.getExposedPorts()).containsExactly(7474);
    }

    @Test
    public void shouldRespectAlreadyDefinedPortMappingsWithoutHttps() {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withExposedPorts(7687, 7474);

        neo4jContainer.configure();

        assertThat(neo4jContainer.getExposedPorts()).containsExactlyInAnyOrder(7474, 7687);
    }

    @Test
    public void shouldDefaultExportBoltHttpAndHttps() {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4");

        neo4jContainer.configure();

        assertThat(neo4jContainer.getExposedPorts()).containsExactlyInAnyOrder(7473, 7474, 7687);
    }

    @Test
    public void shouldRespectCustomWaitStrategy() {
        Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").waitingFor(new CustomDummyWaitStrategy());

        neo4jContainer.configure();

        assertThat(neo4jContainer.getWaitStrategy()).isInstanceOf(CustomDummyWaitStrategy.class);
    }

    @Test
    public void shouldConfigureSingleLabsPlugin() {
        try (
            Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withLabsPlugins(Neo4jLabsPlugin.APOC)
        ) {
            // needs to get called explicitly for setup
            neo4jContainer.configure();

            assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4JLABS_PLUGINS", "[\"apoc\"]");
        }
    }

    @Test
    public void shouldConfigureMultipleLabsPlugins() {
        try (
            // configureLabsPlugins {
            Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
                .withLabsPlugins(Neo4jLabsPlugin.APOC, Neo4jLabsPlugin.BLOOM);
            // }
        ) {
            // needs to get called explicitly for setup
            neo4jContainer.configure();

            assertThat(neo4jContainer.getEnvMap().get("NEO4JLABS_PLUGINS"))
                .containsAnyOf("[\"apoc\",\"bloom\"]", "[\"bloom\",\"apoc\"]");
        }
    }

    @Test
    public void shouldConfigureSingleLabsPluginWithString() {
        try (Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withLabsPlugins("myApoc")) {
            // needs to get called explicitly for setup
            neo4jContainer.configure();

            assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4JLABS_PLUGINS", "[\"myApoc\"]");
        }
    }

    @Test
    public void shouldConfigureMultipleLabsPluginsWithString() {
        try (
            Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withLabsPlugins("myApoc", "myBloom")
        ) {
            // needs to get called explicitly for setup
            neo4jContainer.configure();

            assertThat(neo4jContainer.getEnvMap().get("NEO4JLABS_PLUGINS"))
                .containsAnyOf("[\"myApoc\",\"myBloom\"]", "[\"myBloom\",\"myApoc\"]");
        }
    }

    @Test
    public void shouldCreateRandomUuidBasedPasswords() {
        try (
            // withRandomPassword {
            Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withRandomPassword();
            // }
        ) {
            // It will throw an exception if it's not UUID parsable.
            assertThatNoException().isThrownBy(neo4jContainer::configure);
            // This basically is always true at if the random password is UUID-like.
            assertThat(neo4jContainer.getAdminPassword())
                .satisfies(password -> assertThat(UUID.fromString(password).toString()).isEqualTo(password));
        }
    }

    @Test
    public void shouldWarnOnPasswordTooShort() {
        try (Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4");) {
            Logger logger = (Logger) DockerLoggerFactory.getLogger("neo4j:4.4");
            TestLogAppender testLogAppender = new TestLogAppender();
            logger.addAppender(testLogAppender);
            testLogAppender.start();

            neo4jContainer.withAdminPassword("short");

            testLogAppender.stop();

            assertThat(testLogAppender.passwordTooShortWarningAppeared).isTrue();
        }
    }

    private static class CustomDummyWaitStrategy extends AbstractWaitStrategy {

        @Override
        protected void waitUntilReady() {
            // ehm...ready
        }
    }

    private static class TestLogAppender extends AppenderBase<ILoggingEvent> {

        boolean passwordTooShortWarningAppeared = false;

        @Override
        protected void append(ILoggingEvent eventObject) {
            if (eventObject.getLevel().equals(Level.WARN)) {
                if (
                    eventObject
                        .getMessage()
                        .equals("Your provided admin password is too short and will not work with Neo4j 5.3+.")
                ) {
                    passwordTooShortWarningAppeared = true;
                }
            }
        }
    }

    private static Driver getDriver(Neo4jContainer<?> container) {
        AuthToken authToken = AuthTokens.none();
        if (container.getAdminPassword() != null) {
            authToken = AuthTokens.basic("neo4j", container.getAdminPassword());
        }
        return GraphDatabase.driver(container.getBoltUrl(), authToken);
    }
}

Disable authentication

Authentication can be disabled:

    Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
        .withoutAuthentication()

        

Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withAdminPassword("verySecret");

Random password

A random (UUID-random based) password can be set:

Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withRandomPassword();

Neo4j-Configuration

Neo4j's Docker image needs Neo4j configuration options in a dedicated format. The container takes care of that, and you can configure the database with standard options like the following:

Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
    .withNeo4jConfig("dbms.security.procedures.unrestricted", "apoc.*,algo.*")
    .withNeo4jConfig("dbms.tx_log.rotation.size", "42M");

Add custom plugins

Custom plugins, like APOC, can be copied over to the container from any classpath or host resource like this:

Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
    .withPlugins(MountableFile.forClasspathResource("/custom-plugins/hello-world.jar"))

Whole directories work as well:

Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
    .withPlugins(MountableFile.forClasspathResource("/custom-plugins"))

Add Neo4j Docker Labs plugins

Add any Neo4j Labs plugin from the Neo4j Docker Labs plugin list.

Note

At the moment only the plugins available from the list Neo4j Docker 4.4 are supported by type. If you want to register another supported Neo4j Labs plugin, you have to add it manually by using the method withLabsPlugins(String... neo4jLabsPlugins). Please refer to the list of supported Docker image plugins.

Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
    .withLabsPlugins(Neo4jLabsPlugin.APOC, Neo4jLabsPlugin.BLOOM);

Start the container with a predefined database

If you have an existing database (graph.db) you want to work with, copy it over to the container like this:

Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:3.5.30")
    .withDatabase(MountableFile.forClasspathResource("/test-graph.db"))

Note

The withDatabase method will only work with Neo4j 3.5 and throw an exception if used in combination with a newer version.

Choose your Neo4j license

If you need the Neo4j enterprise license, you can declare your Neo4j container like this:

Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4")
    .withEnterpriseEdition()

This creates a Testcontainers based on the Docker image build with the Enterprise version of Neo4j. The call to withEnterpriseEdition adds the required environment variable that you accepted the terms and condition of the enterprise version. You accept those by adding a file named container-license-acceptance.txt to the root of your classpath containing the text neo4j:3.5.0-enterprise in one line. You'll find more information about licensing Neo4j here: About Neo4j Licenses.

Adding this module to your project dependencies

Add the following dependency to your pom.xml/build.gradle file:

testImplementation "org.testcontainers:neo4j:1.19.0"
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>neo4j</artifactId>
    <version>1.19.0</version>
    <scope>test</scope>
</dependency>

Hint

Add the Neo4j Java driver if you plan to access the Testcontainers via Bolt:

compile "org.neo4j.driver:neo4j-java-driver:4.4.3"
<dependency>
    <groupId>org.neo4j.driver</groupId>
    <artifactId>neo4j-java-driver</artifactId>
    <version>4.4.3</version>
</dependency>