Polish "Add smoketests for relevant Neo4j features"

See gh-190
This commit is contained in:
Moritz Halbritter
2023-10-11 11:41:24 +02:00
parent d8c4090cbb
commit fd8ba7a43f
11 changed files with 84 additions and 168 deletions

View File

@@ -361,8 +361,8 @@ h|nativeTest
|
|data-neo4j
|image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-3.2.x/jobs/data-neo4j-app-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-3.2.x/jobs/data-neo4j-app-test]
|image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-3.2.x/jobs/data-neo4j-native-app-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-3.2.x/jobs/data-neo4j-native-app-test]
|image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-3.0.x/jobs/data-neo4j-app-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-3.0.x/jobs/data-neo4j-app-test]
|image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-3.0.x/jobs/data-neo4j-native-app-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-3.0.x/jobs/data-neo4j-native-app-test]
|
|

View File

@@ -30,18 +30,16 @@ class DataNeo4jApplicationAotTests {
@Test
void annotatedTypesShouldHaveBeenRegistered(AssertableOutput output) {
var expectedLines = List.of("All types are present: ChildNode",
"Id has been populated from DB: \\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}");
List<String> expectedLines = List.of("All types are present: ChildNode", "Id has been populated from DB: \\d+");
assertExpectedLines(output, expectedLines);
}
@Test
void externallyGeneratedFieldsShouldBePopulated(AssertableOutput output) {
var expectedLines = List.of("Generated id is present: \\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}",
"CreatedAt is present: true", "CreatedBy is present: true", "UpdatedAt is absent: false",
"UpdatedBy is absent: false", "Version is 0", "UpdatedAt is now present: true",
"UpdatedBy is now present: true", "Version is now 1");
List<String> expectedLines = List.of("Generated id is present: \\d+", "CreatedAt is present: true",
"CreatedBy is present: true", "UpdatedAt is absent: false", "UpdatedBy is absent: false",
"Version is 0", "UpdatedAt is now present: true", "UpdatedBy is now present: true", "Version is now 1");
assertExpectedLines(output, expectedLines);
}
@@ -49,14 +47,14 @@ class DataNeo4jApplicationAotTests {
@Test
void internallyGeneratedIdsShouldBePopulated(AssertableOutput output) {
Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> {
assertThat(output).hasLineMatching("Internal element id is present: \\d:[\\w\\-]+:\\d")
.hasLineMatching("\\[Reactive\\] Internal element id is present: \\d:[\\w\\-]+:\\d");
assertThat(output).hasLineMatching("Internal element id is present: \\d+")
.hasLineMatching("\\[Reactive\\] Internal element id is present: \\d+");
});
}
@Test
void cypherDSLIntegrationShouldWork(AssertableOutput output) {
var expectedLines = List.of("Loaded 1 movies", "With 1 actors, first named An Actor");
List<String> expectedLines = List.of("Loaded 1 movies", "With 1 actors, first named An Actor");
assertExpectedLines(output, expectedLines);
}
@@ -70,11 +68,10 @@ class DataNeo4jApplicationAotTests {
@Test
void qbeShouldWork(AssertableOutput output) {
Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> {
assertThat(output).hasLineMatching("Found one movie by example with id \\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}")
.hasLineMatching(
"\\[Reactive\\] Found one movie by example with id \\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}");
});
Awaitility.await()
.atMost(Duration.ofSeconds(10))
.untilAsserted(() -> assertThat(output).hasLineMatching("Found one movie by example with id \\d+")
.hasLineMatching("\\[Reactive\\] Found one movie by example with id \\d+"));
}
}

View File

@@ -16,6 +16,8 @@
package com.example.data.neo4j;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.neo4j.driver.Transaction;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;
@@ -32,14 +34,14 @@ public class DatabaseInitializer implements SmartLifecycle {
@Override
public void start() {
if (running) {
if (this.running) {
return;
}
try (var session = driver.session(); var tx = session.beginTransaction()) {
try (Session session = this.driver.session(); Transaction tx = session.beginTransaction()) {
tx.run("MATCH (n:ParentLabel:ChildLabel) DETACH DELETE n");
tx.run("CREATE (:ParentLabel:ChildLabel {id: randomUuid(), name: 'BothLabelsMustBeManagedTypes'})");
tx.run("CREATE (:Movie {id: randomUuid(), version: 0, title: 'A movie', updatedBy: 'A person', updatedAt: localdatetime()}) <-[:ACTED_IN]-(:Person {name: 'An Actor'})");
tx.run("CREATE (:ParentLabel:ChildLabel {id: 1, name: 'BothLabelsMustBeManagedTypes'})");
tx.run("CREATE (:Movie {id: 1, version: 0, title: 'A movie', updatedBy: 'A person', updatedAt: localdatetime()}) <-[:ACTED_IN]-(:Person {name: 'An Actor'})");
tx.commit();
this.running = true;
}
@@ -51,7 +53,7 @@ public class DatabaseInitializer implements SmartLifecycle {
@Override
public boolean isRunning() {
return running;
return this.running;
}
}

View File

@@ -17,8 +17,10 @@ package com.example.data.neo4j;
import java.util.ArrayList;
import java.util.Map;
import java.util.Optional;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Property;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.domain.Example;
import org.springframework.data.neo4j.core.Neo4jTemplate;
@@ -43,7 +45,6 @@ public class ImperativeTestRunner implements CommandLineRunner {
@Override
public void run(String... args) {
annotatedTypesShouldHaveBeenRegistered();
externallyGeneratedFieldsShouldBePopulated();
internallyGeneratedIdsShouldBePopulated();
@@ -52,19 +53,18 @@ public class ImperativeTestRunner implements CommandLineRunner {
}
private void qbeShouldWork() {
log("---- Neo4j Query By Example (QBE) ----");
var optionalMovie = movieRepository.findOne(Example.of(new Movie("A movie")));
Optional<Movie> optionalMovie = this.movieRepository.findOne(Example.of(new Movie("A movie")));
optionalMovie.ifPresent((movie) -> {
log("Found one movie by example with id %s", movie.getId());
});
}
private void cypherDSLIntegrationShouldWork() {
log("---- Neo4j Cypher-DSL integration and relationship population ----");
var title = Cypher.node("Movie").named("movie").property("title");
var movies = new ArrayList<>(movieRepository.findAll(title.contains(Cypher.literalOf("A movie"))));
Property title = Cypher.node("Movie").named("movie").property("title");
ArrayList<Movie> movies = new ArrayList<>(
this.movieRepository.findAll(title.contains(Cypher.literalOf("A movie"))));
log("Loaded %d movies", movies.size());
if (!(movies.isEmpty() || movies.get(0).getActors().isEmpty())) {
@@ -74,19 +74,17 @@ public class ImperativeTestRunner implements CommandLineRunner {
}
private void internallyGeneratedIdsShouldBePopulated() {
log("---- Neo4j internally generated values ----");
var person = new Person();
Person person = new Person();
person.setName("Jane Doe");
person = neo4jTemplate.save(person);
person = this.neo4jTemplate.save(person);
log("Internal element id is present: %s", person.getId());
}
private void externallyGeneratedFieldsShouldBePopulated() {
log("---- Neo4j externally generated values ----");
var movie = transactionTemplate
.execute(tx -> movieRepository.save(new Movie("One Flew Over the Cuckoos Nest")));
Movie movie = this.transactionTemplate
.execute(tx -> this.movieRepository.save(new Movie("One Flew Over the Cuckoos Nest")));
log("Generated id is present: %s", movie.getId());
log("CreatedAt is present: %s", DataNeo4jApplication.FIXED_DATE.equals(movie.getCreatedAt()));
@@ -96,7 +94,7 @@ public class ImperativeTestRunner implements CommandLineRunner {
log("Version is %d", movie.getVersion());
movie.setTitle("One Flew Over the Cuckoo's Nest");
var updatedMovie = transactionTemplate.execute(tx -> movieRepository.save(movie));
Movie updatedMovie = this.transactionTemplate.execute(tx -> this.movieRepository.save(movie));
log("UpdatedAt is now present: %s", DataNeo4jApplication.FIXED_DATE.equals(updatedMovie.getUpdatedAt()));
log("UpdatedBy is now present: %s", "Some person".equals(updatedMovie.getUpdatedBy()));
@@ -104,10 +102,9 @@ public class ImperativeTestRunner implements CommandLineRunner {
}
private void annotatedTypesShouldHaveBeenRegistered() {
log("---- Neo4j Managed types ----");
var optionalResult = this.neo4jTemplate.findOne("MATCH (t:ParentLabel {name: $name}) RETURN t",
Optional<ParentNode> optionalResult = this.neo4jTemplate.findOne("MATCH (t:ParentLabel {name: $name}) RETURN t",
Map.of("name", "BothLabelsMustBeManagedTypes"), ParentNode.class);
optionalResult.ifPresent(node -> {

View File

@@ -17,7 +17,6 @@ package com.example.data.neo4j;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
@@ -35,7 +34,7 @@ public class Movie {
@Id
@GeneratedValue
private final UUID id;
private final Long id;
@Version
private final Long version;
@@ -62,7 +61,7 @@ public class Movie {
}
@PersistenceCreator
Movie(UUID id, Long version, String title, List<Person> actors, LocalDateTime createdAt, String createdBy,
Movie(Long id, Long version, String title, List<Person> actors, LocalDateTime createdAt, String createdBy,
LocalDateTime updatedAt, String updatedBy) {
this.id = id;
this.version = version;
@@ -74,16 +73,16 @@ public class Movie {
this.updatedBy = updatedBy;
}
public UUID getId() {
return id;
public Long getId() {
return this.id;
}
public Long getVersion() {
return version;
return this.version;
}
public String getTitle() {
return title;
return this.title;
}
public void setTitle(String title) {
@@ -91,23 +90,39 @@ public class Movie {
}
public List<Person> getActors() {
return actors;
return this.actors;
}
public LocalDateTime getCreatedAt() {
return createdAt;
return this.createdAt;
}
public String getCreatedBy() {
return createdBy;
return this.createdBy;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
return this.updatedAt;
}
public String getUpdatedBy() {
return updatedBy;
return this.updatedBy;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public void setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
}
}

View File

@@ -15,13 +15,11 @@
*/
package com.example.data.neo4j;
import java.util.UUID;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;
import org.springframework.data.repository.query.QueryByExampleExecutor;
public interface MovieRepository
extends Neo4jRepository<Movie, UUID>, CypherdslConditionExecutor<Movie>, QueryByExampleExecutor<Movie> {
extends Neo4jRepository<Movie, Long>, CypherdslConditionExecutor<Movie>, QueryByExampleExecutor<Movie> {
}

View File

@@ -16,8 +16,6 @@
package com.example.data.neo4j;
import java.util.UUID;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
@@ -32,11 +30,11 @@ public abstract sealed class ParentNode permits ParentNode.ChildNode {
@Id
@GeneratedValue
private UUID id;
private Long id;
private String name;
public UUID getId() {
public Long getId() {
return this.id;
}

View File

@@ -22,20 +22,20 @@ public class Person {
@Id
@GeneratedValue
private String id;
private Long id;
private String name;
public String getId() {
return id;
public Long getId() {
return this.id;
}
public void setId(String id) {
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
return this.name;
}
public void setName(String name) {

View File

@@ -15,13 +15,11 @@
*/
package com.example.data.neo4j;
import java.util.UUID;
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
import org.springframework.data.neo4j.repository.support.ReactiveCypherdslConditionExecutor;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
public interface ReactiveMovieRepository extends ReactiveNeo4jRepository<Movie, UUID>,
public interface ReactiveMovieRepository extends ReactiveNeo4jRepository<Movie, Long>,
ReactiveCypherdslConditionExecutor<Movie>, ReactiveQueryByExampleExecutor<Movie> {
}

View File

@@ -15,9 +15,12 @@
*/
package com.example.data.neo4j;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Property;
import org.neo4j.driver.Driver;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.domain.Example;
@@ -29,25 +32,21 @@ import org.springframework.transaction.reactive.TransactionalOperator;
@Component
public class ReactiveTestRunner implements CommandLineRunner {
private final Driver driver;
private final ReactiveNeo4jTemplate neo4jTemplate;
private final TransactionalOperator transactionalOperator;
private final ReactiveMovieRepository movieRepository;
public ReactiveTestRunner(Driver driver, ReactiveNeo4jTemplate neo4jTemplate,
public ReactiveTestRunner(ReactiveNeo4jTemplate neo4jTemplate,
ReactiveTransactionManager reactiveTransactionManager, ReactiveMovieRepository movieRepository) {
this.driver = driver;
this.neo4jTemplate = neo4jTemplate;
this.transactionalOperator = TransactionalOperator.create(reactiveTransactionManager);
this.movieRepository = movieRepository;
}
@Override
public void run(String... args) throws Exception {
public void run(String... args) {
annotatedTypesShouldHaveBeenRegistered();
externallyGeneratedFieldsShouldBePopulated();
internallyGeneratedIdsShouldBePopulated();
@@ -56,19 +55,19 @@ public class ReactiveTestRunner implements CommandLineRunner {
}
private void qbeShouldWork() {
log("---- [Reactive] Neo4j Query By Example (QBE) ----");
var optionalMovie = movieRepository.findOne(Example.of(new Movie("A movie"))).blockOptional();
Optional<Movie> optionalMovie = this.movieRepository.findOne(Example.of(new Movie("A movie"))).blockOptional();
optionalMovie.ifPresent((movie) -> {
log("[Reactive] Found one movie by example with id %s", movie.getId());
});
}
private void cypherDSLIntegrationShouldWork() {
log("---- [Reactive] Neo4j Cypher-DSL integration and relationship population ----");
var title = Cypher.node("Movie").named("movie").property("title");
var movies = movieRepository.findAll(title.contains(Cypher.literalOf("A movie"))).collectList().block();
Property title = Cypher.node("Movie").named("movie").property("title");
List<Movie> movies = this.movieRepository.findAll(title.contains(Cypher.literalOf("A movie")))
.collectList()
.block();
log("[Reactive] Loaded %d movies", movies.size());
if (!(movies.isEmpty() || movies.get(0).getActors().isEmpty())) {
@@ -78,19 +77,17 @@ public class ReactiveTestRunner implements CommandLineRunner {
}
private void internallyGeneratedIdsShouldBePopulated() {
log("---- [Reactive] Neo4j internally generated values ----");
var person = new Person();
Person person = new Person();
person.setName("Jane Doe");
person = neo4jTemplate.save(person).block();
person = this.neo4jTemplate.save(person).block();
log("[Reactive] Internal element id is present: %s", person.getId());
}
private void externallyGeneratedFieldsShouldBePopulated() {
log("---- [Reactive] Neo4j externally generated values ----");
var movie = movieRepository.save(new Movie("One Flew Over the Cuckoos Nest"))
.as(transactionalOperator::transactional)
Movie movie = this.movieRepository.save(new Movie("One Flew Over the Cuckoos Nest"))
.as(this.transactionalOperator::transactional)
.block();
log("[Reactive] Generated id is present: %s", movie.getId());
@@ -101,7 +98,7 @@ public class ReactiveTestRunner implements CommandLineRunner {
log("[Reactive] Version is %d", movie.getVersion());
movie.setTitle("One Flew Over the Cuckoo's Nest");
var updatedMovie = movieRepository.save(movie).as(transactionalOperator::transactional).block();
Movie updatedMovie = this.movieRepository.save(movie).as(this.transactionalOperator::transactional).block();
log("[Reactive] UpdatedAt is now present: %s",
DataNeo4jApplication.FIXED_DATE.equals(updatedMovie.getUpdatedAt()));
@@ -110,10 +107,9 @@ public class ReactiveTestRunner implements CommandLineRunner {
}
private void annotatedTypesShouldHaveBeenRegistered() {
log("---- [Reactive] Neo4j Managed types ----");
var optionalResult = this.neo4jTemplate
Optional<ParentNode> optionalResult = this.neo4jTemplate
.findOne("MATCH (t:ParentLabel {name: $name}) RETURN t", Map.of("name", "BothLabelsMustBeManagedTypes"),
ParentNode.class)
.blockOptional();
@@ -124,15 +120,6 @@ public class ReactiveTestRunner implements CommandLineRunner {
});
}
private void initializeDatabase(String... args) {
try (var session = driver.session(); var tx = session.beginTransaction()) {
tx.run("MATCH (n:ParentLabel:ChildLabel) DETACH DELETE n");
tx.run("CREATE (:ParentLabel:ChildLabel {id: randomUuid(), name: 'BothLabelsMustBeManagedTypes'})");
tx.run("CREATE (:Movie {id: randomUuid(), version: 0, title: 'A movie', updatedBy: 'A person', updatedAt: localdatetime()}) <-[:ACTED_IN]-(:Person {name: 'An Actor'})");
tx.commit();
}
}
private void log(Object value) {
log(String.valueOf(value), new Object[0]);
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.data.neo4j;
import java.util.List;
import java.util.Set;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.boot.autoconfigure.domain.EntityScanner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.data.neo4j.aot.Neo4jManagedTypes;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.repository.query.CypherdslConditionExecutorImpl;
import org.springframework.data.neo4j.repository.query.ReactiveCypherdslConditionExecutorImpl;
/**
* See https://github.com/spring-projects/spring-data-neo4j/issues/2798 Only needed until
* https://github.com/spring-projects/spring-boot/pull/37574 is resolved Otherwise the
* abscence of {@link ParentNode.ChildNode} will cause application startup failure
*/
@Configuration(proxyBeanMethods = false)
@ImportRuntimeHints(WorkaroundsForMissingTypeHintsAndSB37574Config.MissingRuntimeHints.class)
public class WorkaroundsForMissingTypeHintsAndSB37574Config {
@Bean
public Neo4jManagedTypes neo4jManagedTypes(ApplicationContext applicationContext) throws ClassNotFoundException {
Set<Class<?>> initialEntityClasses = new EntityScanner(applicationContext).scan(Node.class,
RelationshipProperties.class);
return Neo4jManagedTypes.fromIterable(initialEntityClasses);
}
@Bean
public Neo4jMappingContext neo4jMappingContext(Neo4jManagedTypes managedTypes, Neo4jConversions neo4jConversions) {
Neo4jMappingContext context = new Neo4jMappingContext(neo4jConversions);
context.setManagedTypes(managedTypes);
return context;
}
public static class MissingRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection()
.registerTypes(
List.of(TypeReference.of(CypherdslConditionExecutorImpl.class),
TypeReference.of(ReactiveCypherdslConditionExecutorImpl.class)),
builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_METHODS));
}
}
}