diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 9ead6d7..54d3d99 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -40,4 +40,4 @@ jobs:
tag-name: 'v${{ github.event.inputs.version }}'
- uses: aboutbits/github-actions-base/github-create-release@v2
with:
- tag-name: 'v${{ github.event.inputs.version }}'
+ tag-name: '${{ github.event.inputs.version }}'
diff --git a/pom.xml b/pom.xml
index 94e82fb..856b58b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
it.aboutbits
spring-boot-toolbox
- 1.1.0
+ 1.3.0
diff --git a/src/main/java/it/aboutbits/springboot/testing/persistence/PersistenceAssert.java b/src/main/java/it/aboutbits/springboot/testing/persistence/PersistenceAssert.java
index 2a87018..e15ab6b 100644
--- a/src/main/java/it/aboutbits/springboot/testing/persistence/PersistenceAssert.java
+++ b/src/main/java/it/aboutbits/springboot/testing/persistence/PersistenceAssert.java
@@ -9,6 +9,13 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static it.aboutbits.springboot.toolbox.util.CollectUtil.collectToSet;
import static org.assertj.core.api.Assertions.assertThat;
public final class PersistenceAssert {
@@ -23,115 +30,224 @@ private static EntityManager getEntityManager() {
public static , E extends Identified & ChangeAware, M extends Identified & ChangeAware> WriteOperationAsserter assertThatEntity(
@NonNull E before,
@NonNull Class modelClass
+ ) {
+ return new WriteOperationAsserter<>(getEntityManager(), List.of(before), modelClass);
+ }
+
+ @SuppressWarnings("unused")
+ public static , E extends Identified & ChangeAware, M extends Identified & ChangeAware> WriteOperationAsserter assertThatEntity(
+ @NonNull Collection before,
+ @NonNull Class modelClass
) {
return new WriteOperationAsserter<>(getEntityManager(), before, modelClass);
}
+ /**
+ * @deprecated Use {@link #assertThatEntityId(EntityId, Class)} instead.
+ */
+ @Deprecated
@SuppressWarnings("unused")
public static , M extends Identified> WriteOperationIdAsserter assertThatEntity(
@NonNull ID id,
@NonNull Class modelClass
+ ) {
+ return new WriteOperationIdAsserter<>(getEntityManager(), List.of(id), modelClass);
+ }
+
+ @SuppressWarnings("unused")
+ public static , M extends Identified> WriteOperationIdAsserter assertThatEntityId(
+ @NonNull ID id,
+ @NonNull Class modelClass
+ ) {
+ return new WriteOperationIdAsserter<>(getEntityManager(), List.of(id), modelClass);
+ }
+
+ @SuppressWarnings("unused")
+ public static , M extends Identified> WriteOperationIdAsserter assertThatEntityId(
+ @NonNull Collection id,
+ @NonNull Class modelClass
) {
return new WriteOperationIdAsserter<>(getEntityManager(), id, modelClass);
}
+ /**
+ * Batch query entities by their IDs using JPQL
+ */
+ private static , M extends Identified> List batchFindByIds(
+ EntityManager entityManager,
+ Collection ids,
+ Class modelClass
+ ) {
+ if (ids.isEmpty()) {
+ return List.of();
+ }
+
+ return ids.stream()
+ .map(
+ id -> entityManager.find(modelClass, id)
+ )
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ /**
+ * Check if entities with given IDs exist in the database using a count query
+ */
+ private static , M extends Identified> Map batchCheckExistence(
+ EntityManager entityManager,
+ Collection ids,
+ Class modelClass
+ ) {
+ if (ids.isEmpty()) {
+ return Map.of();
+ }
+
+ // Get existing entities
+ var existingEntities = batchFindByIds(entityManager, ids, modelClass);
+ var existingIds = collectToSet(existingEntities, Identified::getId);
+
+ // Map each ID to its existence status
+ return ids.stream()
+ .collect(Collectors.toMap(
+ id -> id,
+ existingIds::contains
+ ));
+ }
+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static final class WriteOperationAsserter, E extends Identified & ChangeAware, M extends Identified & ChangeAware> {
private final EntityManager entityManager;
- private final E entity;
+ private final Collection entity;
private final Class modelClass;
@SuppressWarnings("unused")
public void hasBeenCreatedInDatabase() {
entityManager.clear();
- var savedInstance = getSavedInstance();
- assertThat(
- savedInstance.getId()
- ).isNotNull();
+ var ids = collectToSet(entity, Identified::getId);
+ var savedInstances = batchFindByIds(entityManager, ids, modelClass);
+
+ for (var savedInstance : savedInstances) {
+ assertThat(
+ savedInstance.getId()
+ ).isNotNull();
+
+ assertThat(
+ savedInstance.getCreatedAt()
+ ).isNotNull();
+ }
- assertThat(
- savedInstance.getCreatedAt()
- ).isNotNull();
+ // Verify all entities were found
+ assertThat(savedInstances).hasSize(entity.size());
}
@SuppressWarnings("unused")
public void hasBeenUpdatedInDatabase() {
entityManager.clear();
- var savedInstance = getSavedInstance();
- assertThat(
- savedInstance.getUpdatedAt()
- ).isAfter(
- entity.getUpdatedAt()
- );
+ var ids = collectToSet(entity, Identified::getId);
+ var savedInstances = batchFindByIds(entityManager, ids, modelClass);
+
+ // Create a map for easy lookup of original entities by ID
+ var originalByIdMap = entity.stream()
+ .collect(Collectors.toMap(Identified::getId, e -> e));
+
+ for (var savedInstance : savedInstances) {
+ var originalEntity = originalByIdMap.get(savedInstance.getId());
+ assertThat(originalEntity).isNotNull();
+
+ assertThat(
+ savedInstance.getUpdatedAt()
+ ).isAfter(
+ originalEntity.getUpdatedAt()
+ );
+ }
}
@SuppressWarnings("unused")
public void hasNotChangedInDatabase() {
entityManager.clear();
- var savedInstance = getSavedInstance();
- assertThat(savedInstance).isNotNull();
+ var ids = collectToSet(entity, Identified::getId);
+ var savedInstances = batchFindByIds(entityManager, ids, modelClass);
+
+ // Create a map for easy lookup of original entities by ID
+ var originalByIdMap = entity.stream()
+ .collect(Collectors.toMap(Identified::getId, e -> e));
+
+ for (var savedInstance : savedInstances) {
+ var originalEntity = originalByIdMap.get(savedInstance.getId());
- assertThat(
- savedInstance.getUpdatedAt()
- ).isEqualTo(
- entity.getUpdatedAt()
- );
+ assertThat(savedInstance).isNotNull();
+ assertThat(originalEntity).isNotNull();
+
+ assertThat(
+ savedInstance.getUpdatedAt()
+ ).isEqualTo(
+ originalEntity.getUpdatedAt()
+ );
+ }
}
@SuppressWarnings("unused")
public void isAbsentInDatabase() {
entityManager.clear();
- var savedInstance = getSavedInstance();
- assertThat(savedInstance).isNull();
+ var ids = collectToSet(entity, Identified::getId);
+ var existenceMap = batchCheckExistence(entityManager, ids, modelClass);
+
+ for (var id : ids) {
+ assertThat(existenceMap.get(id))
+ .as("Entity with ID %s should be absent from database", id)
+ .isFalse();
+ }
}
@SuppressWarnings("unused")
public void isPresentInDatabase() {
entityManager.clear();
- var savedInstance = getSavedInstance();
- assertThat(savedInstance).isNotNull();
- }
+ var ids = collectToSet(entity, Identified::getId);
+ var existenceMap = batchCheckExistence(entityManager, ids, modelClass);
- private M getSavedInstance() {
- return entityManager.find(
- modelClass,
- entity.getId()
- );
+ for (var id : ids) {
+ assertThat(existenceMap.get(id))
+ .as("Entity with ID %s should be present in database", id)
+ .isTrue();
+ }
}
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static final class WriteOperationIdAsserter, M extends Identified> {
private final EntityManager entityManager;
- private final ID id;
+ private final Collection id;
private final Class modelClass;
@SuppressWarnings("unused")
public void isAbsentInDatabase() {
entityManager.clear();
- var savedInstance = getSavedInstance();
- assertThat(savedInstance).isNull();
+ var existenceMap = batchCheckExistence(entityManager, id, modelClass);
+
+ for (var entityId : id) {
+ assertThat(existenceMap.get(entityId))
+ .as("Entity with ID %s should be absent from database", entityId)
+ .isFalse();
+ }
}
@SuppressWarnings("unused")
public void isPresentInDatabase() {
entityManager.clear();
- var savedInstance = getSavedInstance();
- assertThat(savedInstance).isNotNull();
- }
+ var existenceMap = batchCheckExistence(entityManager, id, modelClass);
- private M getSavedInstance() {
- return entityManager.find(
- modelClass,
- id
- );
+ for (var entityId : id) {
+ assertThat(existenceMap.get(entityId))
+ .as("Entity with ID %s should be present in database", entityId)
+ .isTrue();
+ }
}
}
}
diff --git a/src/main/java/it/aboutbits/springboot/testing/testdata/base/AllTestDataReader.java b/src/main/java/it/aboutbits/springboot/testing/testdata/base/AllTestDataReader.java
new file mode 100644
index 0000000..bd3e6b4
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/testdata/base/AllTestDataReader.java
@@ -0,0 +1,68 @@
+package it.aboutbits.springboot.testing.testdata.base;
+
+import lombok.NonNull;
+
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public abstract class AllTestDataReader- {
+ public ITEM returnFirst() {
+ return (ITEM) this.returnAll().getFirst();
+ }
+
+ public List
- returnAll() {
+ return this.fetch();
+ }
+
+ @SafeVarargs
+ public final List
- returnSorted(@NonNull Comparator
- ... comparators) {
+ if (comparators.length == 0) {
+ throw new IllegalArgumentException("At least one comparator must be provided");
+ }
+
+ var combinedComparator = comparators[0];
+ for (var i = 1; i < comparators.length; i++) {
+ combinedComparator = combinedComparator.thenComparing(comparators[i]);
+ }
+
+ return returnAll().stream().sorted(combinedComparator).toList();
+ }
+
+ @SafeVarargs
+ @SuppressWarnings("unchecked")
+ public final > List
- returnSorted(@NonNull Function
- >... comparators) {
+ if (comparators.length == 0) {
+ throw new IllegalArgumentException("At least one comparator must be provided");
+ }
+
+ var combinedComparator = Comparator.comparing((Function
- ) comparators[0]);
+ for (var i = 1; i < comparators.length; i++) {
+ combinedComparator = combinedComparator.thenComparing((Function
- ) comparators[i]);
+ }
+
+ return returnAll().stream().sorted(combinedComparator).toList();
+ }
+
+ public > AllAndFiltered
- returnFiltered(@NonNull Predicate
- predicate) {
+ var all = this.returnAll();
+ return new AllAndFiltered
- (
+ all,
+ all.stream().filter(predicate).toList(),
+ all.stream().filter(item -> !predicate.test(item)).toList()
+ );
+ }
+
+ public Set
- returnSet() {
+ return new HashSet<>(this.returnAll());
+ }
+
+ protected abstract List
- fetch();
+
+ public record AllAndFiltered(@NonNull List all, @NonNull List filtered, @NonNull List other) {
+
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/testdata/base/TestDataCreator.java b/src/main/java/it/aboutbits/springboot/testing/testdata/base/TestDataCreator.java
index bcd3c08..e9ef674 100644
--- a/src/main/java/it/aboutbits/springboot/testing/testdata/base/TestDataCreator.java
+++ b/src/main/java/it/aboutbits/springboot/testing/testdata/base/TestDataCreator.java
@@ -30,14 +30,33 @@ public List
- returnAll() {
return create();
}
- public List
- returnSorted(@NonNull Comparator
- comparator) {
- return returnAll().stream().sorted(comparator).toList();
+ @SafeVarargs
+ public final List
- returnSorted(@NonNull Comparator
- ... comparators) {
+ if (comparators.length == 0) {
+ throw new IllegalArgumentException("At least one comparator must be provided");
+ }
+
+ var combinedComparator = comparators[0];
+ for (var i = 1; i < comparators.length; i++) {
+ combinedComparator = combinedComparator.thenComparing(comparators[i]);
+ }
+
+ return returnAll().stream().sorted(combinedComparator).toList();
}
- public > List
- returnSorted(@NonNull Function
- comparator) {
- return returnAll().stream()
- .sorted(Comparator.comparing(comparator))
- .toList();
+ @SafeVarargs
+ @SuppressWarnings("unchecked")
+ public final > List
- returnSorted(@NonNull Function
- >... comparators) {
+ if (comparators.length == 0) {
+ throw new IllegalArgumentException("At least one comparator must be provided");
+ }
+
+ var combinedComparator = Comparator.comparing((Function
- ) comparators[0]);
+ for (var i = 1; i < comparators.length; i++) {
+ combinedComparator = combinedComparator.thenComparing((Function
- ) comparators[i]);
+ }
+
+ return returnAll().stream().sorted(combinedComparator).toList();
}
public Set
- returnSet() {