Testing Spring Data Neo4j Components

  • Neo4j provides a test harness which provides APIs to start and stop an embedded Neo4j server by programmatic approach.
  • Testcontainers is a generic solution to run Docker containers for the testing framework, it is easy to start a Neo4j database in a Docker container when testing Spring Data Neo4j repositories.

Test with Neo4j test harness

Add the following dependency into your pom.xml.

<dependency>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>${neo4j-harness.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
</exclusion>
</exclusions>
</dependency>
@DataNeo4jTest
@Transactional(propagation = Propagation.NEVER)
@Slf4j
public class PostRepositoryWithNeo4jHarnessTest {
private static Neo4j embeddedDatabaseServer; @BeforeAll
static void initializeNeo4j() {
embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer()//disable http server
.build();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", embeddedDatabaseServer::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> null);
}
@AfterAll
static void stopNeo4j() {
embeddedDatabaseServer.close();
}
...
}
  • We use the JUnit 5 lifecycle hooks, such as beforeAll and afterAll to start and stop an embedded Neo4j server.
  • Use a static method annotated with @DynamicPropertySource to bind the Neo4j properties to the Spring test context.

Test with Testcontainers

Testcontainers provides a simple programmatic API abstraction for you to bootstrap a Docker container in your testing codes.

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>neo4j</artifactId>
<scope>test</scope>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
  • The junit-jupiter artifact is used to integrate TestContainers with JUnit 5 platform.
  • The neo4j artifact provides APIs to compose a Neo4j Docker container.
@SpringBootTest
//@DataNeo4jTest
@Testcontainers
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
public class PostRepositoryWithTestContainersTest {
@Container
static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.0")
.withStartupTimeout(Duration.ofMinutes(5));
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", neo4jContainer::getBoltUrl);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", neo4jContainer::getAdminPassword);
}
@Autowired
private PostRepository posts;
@BeforeEach
public void setup() throws IOException {
log.debug("running setup.....,");
this.posts.deleteAll()
.thenMany(testSaveMethod())
.log()
.thenMany(testFoundMethod())
.log()
.blockLast(Duration.ofSeconds(5));// to make the tests work
}
private Flux<Post> testSaveMethod() {
var data = Stream.of("Post one", "Post two")
.map(title -> Post.builder().title(title).content("The content of " + title).build())
.collect(Collectors.toList());
return Flux.fromIterable(data)
.flatMap(it -> this.posts.save(it));
}
private Flux<Post> testFoundMethod() {
return this.posts
.findAll(Example.of(Post.builder().title("Post one").build()));
}
@AfterEach
void teardown() {
}
@Test
void testAllPosts() {
posts.findAll().sort(Comparator.comparing(post -> post.getTitle()))
.as(StepVerifier::create)
.consumeNextWith(p -> assertEquals("Post one", p.getTitle()))
.consumeNextWith(p -> assertEquals("Post two", p.getTitle()))
.verifyComplete();
}
}
  • A test class is annotated with a general @SpringBootTest annotation(will load all configurations) or a @DataNeo4jTest annotation. When using @DataNeo4jTest, you have to add an extra @Transactional(propagation = Propagation.NEVER), check spring-boot issue#23630 for more details.
  • A @Testcontainers is added on the class level, thus the Testcontainers facilities will contribute the test lifecycle.
  • A static @Container resource is defined, it will be initialized before the test execution.
  • By default, JUnit 5 uses a PER_METHOD strategy to bootstrap a test, if you set a global PER_CLASS strategy in the junit-platform.properties, add a @TestInstance(TestInstance.Lifecycle.PER_METHOD) to override it.
  • A static method annotated with @DynamicPropertySource is used to bind properties from the running Docker container to the Spring environmental variables before the test is running.
  • You can inject your Repository beans, and the Neo4j specific ReactiveNeo4jOperations, ReactiveNeo4jClient, Driver beans etc. in a @DataNeo4jTest annotated test directly.
  • Generally, you can add @BeforeEach, @AfterEach methods to hook the JUnit test lifecycle.
  • In the @Test method, we usually utilizes reactor's StepVerifier to assert the result.
  • Preparing Spring test context.
  • Check if there is a Docker image existed, if not download it firstly.
  • Startup a Docker container for the test.
  • Bind the Docker instance properties to Spring environmental variables.
  • Inject Spring resources.
  • Executing test, if there are some hooks, executing hooks before and after the test execution.
  • Shutdown the Docker container and clean up Spring test context.
@DataNeo4jTest
@Transactional(propagation = Propagation.NEVER)
@ContextConfiguration(initializers = PostRepositoryTest.TestContainerInitializer.class)
@Slf4j
public class PostRepositoryTest {
static class TestContainerInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
final Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.0").withoutAuthentication();
neo4jContainer.start();
configurableApplicationContext
.addApplicationListener((ApplicationListener<ContextClosedEvent>) event -> neo4jContainer.stop());
TestPropertyValues
.of(
"spring.neo4j.uri=" + neo4jContainer.getBoltUrl(),
"spring.neo4j.authentication.username=neo4j",
"spring.neo4j.authentication.password=" + neo4jContainer.getAdminPassword()
)
.applyTo(configurableApplicationContext.getEnvironment());
}
}
...
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Hantsy

Hantsy

Self-employed technical consultant, solution architect and full-stack developer, open source contributor, freelancer and remote worker