Embedded Neo4j with Integration Tests

Neo4j is a Graph Database Management System.

The data elements Neo4j stores are nodes. Each node can have a relationship to another node by an Edge with attributes such as (Belongs to), (Has).

Some argue that it is faster than RDBMS, I say do a POC and see how well it handles >5 Million data with different test scenarios (Find the 10th Cousin of X Person), vs (Find all users where date of birth between 1982,2000 and are only males).

Spring Documentation actually recommends 2 ways to do Embedded Neo4j Testing,

We recommend one of two options: either use the Neo4j Testcontainers module or the Neo4j test harness. While Testcontainers is a known project with modules for a lot of different services, Neo4j test harness is rather unknown. It is an embedded instance that is especially useful when testing stored procedures as described in Testing your Neo4j-based Java application. The test harness can however be used to test an application as well. As it brings up a database inside the same JVM as your application, performance and timings may not resemble your production setup.

In this example, we will be using a 3rd option which is an embedded instance of the API DatabaseManagementService.

Maven Dependencies:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>neo4j-Embedded-spring-data</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>neo4j-Embedded-spring-data</name>

    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-neo4j</artifactId>

        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Neo4j Dependencies -->

        <dependency>
            <groupId>org.neo4j</groupId>
            <artifactId>neo4j</artifactId>
            <version>5.26.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

TestConfiguration.

We are using a single TestConfiguration across different Integration Tests, so that we load the initial setup data only once, and share that same context across those Tests.

package com.example.spring.neo4j;


import com.example.spring.neo4j.model.Course;
import com.example.spring.neo4j.model.Major;
import com.example.spring.neo4j.model.Student;
import com.example.spring.neo4j.repository.CourseRepository;
import com.example.spring.neo4j.repository.MajorRepository;
import com.example.spring.neo4j.repository.StudentRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.neo4j.configuration.connectors.BoltConnector;
import org.neo4j.configuration.helpers.SocketAddress;
import org.neo4j.dbms.api.DatabaseManagementService;
import org.neo4j.dbms.api.DatabaseManagementServiceBuilder;
import org.neo4j.io.fs.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.function.Function;


@TestConfiguration
public class Neo4JEmbeddedBaseTestConfiguration {

    @Autowired
    private StudentRepository studentRepository;

    @Autowired
    private CourseRepository courseRepository;

    @Autowired
    private MajorRepository majorRepository;


    private final ObjectMapper objectMapper = new ObjectMapper();

    private DatabaseManagementService managementService;


    @PostConstruct
    void initialize() throws IOException {

        Path DB_PATH = Path.of("target/neo4j-store-with-bolt");

        FileUtils.deleteDirectory(DB_PATH);

        managementService = new DatabaseManagementServiceBuilder(DB_PATH)
                .setConfig(Map.of(
                        BoltConnector.enabled, true,
                        BoltConnector.listen_address, new SocketAddress("localhost", 7687)))
                .build();

        populateGraphData();
    }

    @PreDestroy
    void closeConnections(){
        managementService.shutdown();
    }

    private void populateGraphData() throws JsonProcessingException {

        List<Course> courses = objectMapper.readValue(getCourseData(), new TypeReference<>() {});


        Function<Course, Course> applyPrerequisites = (course) ->{
            switch (course.getCourseId()){
                case "CMPP 4010" -> course.setPrerequisite(courses.get(2));
                case "CMPS 4000" -> course.setPrerequisite(courses.get(6));
                case "CPSY 4000" -> course.setPrerequisite(courses.get(7));
            }
            return course;
        };


        List<Course> persistedCourses = courseRepository.saveAll(courses.stream().map(applyPrerequisites).toList());


        Major softwareEngineering = new Major();
        softwareEngineering.setName("Bachelors SE");
        softwareEngineering.setMandatoryCourses(persistedCourses);

        majorRepository.save(softwareEngineering);


        Major softwareEngineeringShortDiploma = new Major();
        softwareEngineeringShortDiploma.setName("Diploma SE");
        softwareEngineeringShortDiploma.setMandatoryCourses(persistedCourses.subList(0,4)); //Short list of Courses, since it is a diploma

        majorRepository.save(softwareEngineeringShortDiploma);


        Student john = new Student("ID0001", "John", "Doe");

        john.setCompletedCourses(persistedCourses.subList(0, 4));
        john.setOngoingCourses(persistedCourses.subList(5, 8));
        john.setMajor(softwareEngineering);


        Student susan = new Student("ID0002", "Susan", "Mustafa");


        susan.setMajor(softwareEngineering);
        susan.setCompletedCourses(persistedCourses.subList(2, 5));
        susan.setOngoingCourses(persistedCourses.subList(6,8));


        Student james = new Student("ID0003", "James", "Bond");
        james.setCompletedCourses(persistedCourses.subList(0,2));
        james.setOngoingCourses(persistedCourses.subList(2,4));
        james.setMajor(softwareEngineeringShortDiploma);


        Student lazyStudent = new Student("ID0004", "Lazy", "Dude");

        studentRepository.save(john);  //Bachelors
        studentRepository.save(susan); //Bachelors
        studentRepository.save(james); //Only Diploma here.


        studentRepository.save(lazyStudent); //This student is a lonely node.  No edge relations.

    }


    private String getCourseData() {

        return """
                			[{
                						"courseId" : "CMPP 3020",
                						"name" : "Advanced Programming Language Concepts"
                  
                			},
                			{
                						"courseId" : "CMPP 4010",
                						"name" : "Applied Software Development"
                  	
                			},
                			{
                						"courseId" : "CMPS 3000",
                						"name" : "Computational Thinking and Problem Solving"
                			},
                			{
                						"courseId" : "CMPS 4000",
                						"name" :  "Computational Intelligence"
                			},
                			{
                						"courseId" : "CPSY 3000",
                						"name" : "Computer Architecture"
                  	
                			},
                			{
                						"courseId" : "CPSY 4000",
                						"name" : "Algorithms"
                  	
                			},
                			{
										"courseId" : "DATA 3000",
										"name" : "Data Structures"    	
                			},
                			{
										"courseId" : "DSGN 4000",
										"name" : "Advanced User Interface"
                  	
                			},
                			{
                  	
										"courseId" : "MATH 3000",
										"name" : "Math For Developers"
                			}
                  	  
                			]
                """;
    }


}

 managementService = new DatabaseManagementServiceBuilder(DB_PATH)
                .setConfig(Map.of(
                        BoltConnector.enabled, true,
                        BoltConnector.encryption_level, BoltConnector.EncryptionLevel.DISABLED,
                        BoltConnector.listen_address, new SocketAddress("localhost", 7687)))
                .build();

Start an embedded instance on localhost:7687,

DB_PATH is a local directory under your /target. It is where your neo4j database will be stored in.

Encryption is DISABLED, as later you when you want to use Neo4j Browser and connect to your database directory, it will not ask you for a Username/Password. Please dont do this for a production instance.

Neo4j Repositories:
package com.example.spring.neo4j.repository;

import com.example.spring.neo4j.model.Course;
import org.springframework.data.neo4j.repository.Neo4jRepository;

public interface CourseRepository extends Neo4jRepository<Course, Long> {

    public Course getByCourseId(String courseId);

    public Course findByPrerequisiteName(String courseName);
}

package com.example.spring.neo4j.repository;

import com.example.spring.neo4j.model.Major;
import org.springframework.data.neo4j.repository.Neo4jRepository;

import java.util.List;

public interface MajorRepository extends Neo4jRepository<Major, Long> {


    List<Major> findAllByMandatoryCourses_CourseId(String prerequisiteCourseId);


    //3 Layers down.  Major -- Mandatory Courses -- Prerequisites -- Course Id
    List<Major> findAllByMandatoryCourses_Prerequisite_CourseId(String prerequisiteCourseId);


}
package com.example.spring.neo4j.repository;

import com.example.spring.neo4j.model.Student;
import org.springframework.data.neo4j.repository.Neo4jRepository;

import java.util.List;

public interface StudentRepository extends Neo4jRepository<Student, Long> {

    List<Student> findAllByCompletedCourses_CourseId(String courseId);

    List<Student> findAllByOngoingCourses_CourseId(String courseId);

    //3 Layers down, Find all Students that have a major, which has a mandatory course of *id*
    List<Student> findAllByMajor_MandatoryCourses_CourseId(String courseId);

}
Integration Tests:
package com.example.spring.neo4j;

import com.example.spring.neo4j.model.Major;
import com.example.spring.neo4j.repository.MajorRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
import org.springframework.test.context.ContextConfiguration;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

@ContextConfiguration(classes = Neo4JEmbeddedBaseTestConfiguration.class)
@DataNeo4jTest
class MajorNeo4jEmbeddedIntegrationTest {

    @Autowired
    private MajorRepository majorRepository;

    @Test
    void verifyMajorByMandatoryCourseIds() {

        List<Major> allMajorsWithMandatoryCourseId_CMPP_3020 = majorRepository.findAllByMandatoryCourses_CourseId("CMPP 3020");

        assertEquals(2, allMajorsWithMandatoryCourseId_CMPP_3020.size());

    }
    @Test
    void verifyMajorByPreRequisiteCourseId() {

        List<Major> allMajorsWithAPrerequisiteCourseId_CMPS_3000 = majorRepository.findAllByMandatoryCourses_Prerequisite_CourseId("CMPS 3000"); //2 Majors
        assertEquals(2, allMajorsWithAPrerequisiteCourseId_CMPS_3000.size());

        List<Major> allMajorsWithAPrerequisiteCourseId_DSGN_4000 = majorRepository.findAllByMandatoryCourses_Prerequisite_CourseId("DSGN 4000"); //1 Majors - Bachelors

        assertEquals(1, allMajorsWithAPrerequisiteCourseId_DSGN_4000.size());
        assertEquals("Bachelors SE", allMajorsWithAPrerequisiteCourseId_DSGN_4000.get(0).getName());

    }
}

package com.example.spring.neo4j;

import com.example.spring.neo4j.model.Student;
import com.example.spring.neo4j.repository.StudentRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
import org.springframework.test.context.ContextConfiguration;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ContextConfiguration(classes = Neo4JEmbeddedBaseTestConfiguration.class)
@DataNeo4jTest
class StudentNeo4jEmbeddedIntegrationTests {

    @Autowired
    private StudentRepository studentRepository;

    @Test
    void verifyStudentsByCompletedCourses() {


        List<Student> studentsCompleted_CMPP_4010 = studentRepository.findAllByCompletedCourses_CourseId("CMPP 4010"); //Expect  John
        List<Student> studentsCompleted_CMPS_3000 = studentRepository.findAllByCompletedCourses_CourseId("CMPS 3000"); //Expect John/Susan
        List<Student> studentsCompleted_CPSY_3000 = studentRepository.findAllByCompletedCourses_CourseId("CPSY 3000"); //Expect Susan
        List<Student> studentsCompleted_MATH_3000 = studentRepository.findAllByCompletedCourses_CourseId("MATH 3000"); //Expect None


        assertEquals("John", studentsCompleted_CMPP_4010.get(0).getFirstName());

        assertEquals("John", studentsCompleted_CMPS_3000.get(0).getFirstName());
        assertEquals("Susan", studentsCompleted_CMPS_3000.get(1).getFirstName());

        assertEquals("Susan", studentsCompleted_CPSY_3000.get(0).getFirstName());

        assertEquals(0, studentsCompleted_MATH_3000.size());


    }

    @Test
    void verifyStudentsByOngoingCourses(){
        List<Student> studentsOngoing_CPSY_4000 = studentRepository.findAllByOngoingCourses_CourseId("CPSY 4000"); //John
        List<Student> studentsOngoing_DSGN_4000 = studentRepository.findAllByOngoingCourses_CourseId("DSGN 4000"); //John/Susan
        List<Student> studentsOngoing_CMPP_3020 = studentRepository.findAllByOngoingCourses_CourseId("CMPP 3020"); //None


        assertEquals("John", studentsOngoing_CPSY_4000.get(0).getFirstName());

        assertEquals("John", studentsOngoing_DSGN_4000.get(0).getFirstName() );
        assertEquals("Susan", studentsOngoing_DSGN_4000.get(1).getFirstName());

        assertEquals(0, studentsOngoing_CMPP_3020.size());

    }

    @Test
    void verifyStudentsByMajorWithAGivenCourseId(){

        //Both Diploma and Bachelors majors should have this course as mandatory.
        List<Student> studentsWithMajorThatHas_CMPS_4000 = studentRepository.findAllByMajor_MandatoryCourses_CourseId("CMPS 4000");

        assertEquals(3, studentsWithMajorThatHas_CMPS_4000.size() );

    }


}

After all Tests pass, and your build is finished, you can actually connect to the (Embedded neo4j-store-with-bolt database) through Neo4j Browser.

Using Docker for example: –env NEO4J_dbms_security_auth__enabled=false so we dont need to authenticate.

You can see the various neo4j -to- docker configuration mapping.

docker run --rm -p 7474:7474 -p 7687:7687 -v $PWD/target/neo4j-store-with-bolt/data:/data --name neo4j --env NEO4J_dbms_security_auth__enabled=false neo4j

neo4j-embedded-spring-data% docker run --rm -p 7474:7474 -p 7687:7687 -v $PWD/target/neo4j-store-with-bolt/data:/data --name neo4j --env NEO4J_dbms_security_auth__enabled=false neo4j
Warning: Folder mounted to "/data" is not writable from inside container. Changing folder owner to neo4j.
2025-02-06 11:13:20.050+0000 INFO  Logging config in use: File '/var/lib/neo4j/conf/user-logs.xml'
2025-02-06 11:13:20.057+0000 INFO  Starting...
2025-02-06 11:13:20.517+0000 INFO  This instance is ServerId{da706129} (da706129-6714-4f0f-8fd8-74821edb1ea6)
2025-02-06 11:13:21.006+0000 INFO  ======== Neo4j 5.26.0 ========
2025-02-06 11:13:21.824+0000 INFO  Anonymous Usage Data is being sent to Neo4j, see https://neo4j.com/docs/usage-data/
2025-02-06 11:13:21.856+0000 INFO  Bolt enabled on 0.0.0.0:7687.
2025-02-06 11:13:22.137+0000 INFO  HTTP enabled on 0.0.0.0:7474.
2025-02-06 11:13:22.137+0000 INFO  Remote interface available at http://localhost:7474/
2025-02-06 11:13:22.139+0000 INFO  id: B5A013FDE16C1FA9264B70824E80B66303E70ED96E86F8C134BF6B5600751635
2025-02-06 11:13:22.139+0000 INFO  name: system
2025-02-06 11:13:22.139+0000 INFO  creationDate: 2025-02-06T11:12:57.05Z
2025-02-06 11:13:22.139+0000 INFO  Started.


Neo4J Browser: http://localhost:7474/browser/preview/. Click Connect. No need to enter a password as we disabled auth previously

Github

Leave a comment