Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : enhance tests #1555

Merged
merged 1 commit into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@

import com.example.keysetpagination.entities.Animal;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface AnimalRepository extends JpaRepository<Animal, Long>, CustomRepository<Animal> {}
public interface AnimalRepository
extends JpaRepository<Animal, Long>, CustomRepository<Animal>, JpaSpecificationExecutor<Animal> {}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface CustomRepository<T> {
/**
* Finds all entities matching the given specification using keyset pagination.
*
* @param spec The specification to filter entites
* @param spec The specification to filter entities
* @param pageRequest The pagination information
* @param scrollPosition The current position in the result set
* @param entityClass The entity class on which operation should occur
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
import jakarta.persistence.criteria.Root;
import jakarta.persistence.metamodel.ManagedType;
import jakarta.persistence.metamodel.Metamodel;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.springframework.data.jpa.domain.Specification;
Expand Down Expand Up @@ -77,7 +80,9 @@ private Predicate createPredicate(SearchCriteria searchCriteria, Root<T> root, C
criteriaBuilder::and);
case LIKE, CONTAINS -> combinePredicates(
typedValues,
value -> criteriaBuilder.like(root.get(fieldName), "%" + value + "%"),
value -> criteriaBuilder.like(
criteriaBuilder.lower(root.get(fieldName)),
"%" + value.toString().toLowerCase() + "%"),
criteriaBuilder::or);
case STARTS_WITH -> combinePredicates(
typedValues, value -> criteriaBuilder.like(root.get(fieldName), value + "%"), criteriaBuilder::and);
Expand All @@ -102,6 +107,10 @@ private Object convertToType(String value, Class<?> fieldType) {
try {
if (fieldType.equals(String.class)) {
return value;
} else if (fieldType.equals(BigDecimal.class)) {
return new BigDecimal(value);
} else if (fieldType.equals(UUID.class)) {
return UUID.fromString(value);
} else if (fieldType.equals(Integer.class) || fieldType.equals(int.class)) {
return Integer.valueOf(value);
} else if (fieldType.equals(Long.class) || fieldType.equals(long.class)) {
Expand All @@ -111,9 +120,9 @@ private Object convertToType(String value, Class<?> fieldType) {
} else if (fieldType.equals(Boolean.class) || fieldType.equals(boolean.class)) {
return Boolean.valueOf(value);
} else if (fieldType.equals(LocalDate.class)) {
return LocalDate.parse(value);
return LocalDate.parse(value, DateTimeFormatter.ISO_DATE);
} else if (fieldType.equals(LocalDateTime.class)) {
return LocalDateTime.parse(value);
return LocalDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
} else if (Enum.class.isAssignableFrom(fieldType)) {
return Enum.valueOf((Class<Enum>) fieldType, value);
} else {
Expand All @@ -129,10 +138,14 @@ private Predicate combinePredicates(
List<Object> values,
Function<Object, Predicate> predicateFunction,
BiFunction<Predicate, Predicate, Predicate> combiner) {
if (values.size() == 1) {
return predicateFunction.apply(values.getFirst());
}
return values.stream()
.map(predicateFunction)
.reduce(combiner::apply)
.orElseThrow(() -> new IllegalArgumentException("No predicates could be generated from values"));
.orElseThrow(() -> new IllegalArgumentException(
String.format("No predicates could be generated from values: %s", values)));
}

private void validateMetadata(List<SearchCriteria> searchCriteriaList, Class<T> entityClass) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,120 +1,165 @@
package com.example.keysetpagination.services;
package com.example.keysetpagination.repository;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.example.keysetpagination.common.AbstractIntegrationTest;
import com.example.keysetpagination.common.ContainersConfig;
import com.example.keysetpagination.config.JpaAuditConfig;
import com.example.keysetpagination.entities.Animal;
import com.example.keysetpagination.model.query.QueryOperator;
import com.example.keysetpagination.model.query.SearchCriteria;
import com.example.keysetpagination.repositories.AnimalRepository;
import com.example.keysetpagination.services.EntitySpecification;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.domain.Specification;

class EntitySpecificationIntTest extends AbstractIntegrationTest {
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DataJpaTest
@Import({ContainersConfig.class, JpaAuditConfig.class})
class EntitySpecificationTest {

private EntitySpecification<Animal> entitySpecification;

@PersistenceContext
private EntityManager entityManager;

@BeforeEach
@Autowired
private AnimalRepository animalRepository;

@BeforeAll
void setUp() {
entitySpecification = new EntitySpecification<>(entityManager);
// Add test data
Animal mammal = new Animal().setName("Lion").setType("Mammal").setHabitat("Savanna");
Animal bird = new Animal().setName("Eagle").setType("Bird").setHabitat("Forest");
Animal fish = new Animal().setName("Shark").setType("Fish").setHabitat("Ocean");
animalRepository.saveAll(List.of(mammal, bird, fish));
}

@Test
void shouldBuildSpecificationForEQOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.EQ, "type", List.of("Mammal"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal");
}

@Test
void shouldBuildSpecificationForNEOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.NE, "type", List.of("Reptile"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal", "Bird", "Fish");
}

@Test
void shouldBuildSpecificationForLTOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.LT, "id", List.of("5"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal", "Bird", "Fish");
}

@Test
void shouldBuildSpecificationForGTOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.GT, "id", List.of("2"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Fish");
}

@Test
void shouldBuildSpecificationForGTEOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.GTE, "id", List.of("3"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Fish");
}

@Test
void shouldBuildSpecificationForLTEOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.LTE, "id", List.of("7"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal", "Bird", "Fish");
}
Comment on lines +71 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid using auto-generated IDs in test assertions

Relying on auto-generated IDs like "id" in test criteria can lead to flaky tests since IDs may differ across environments or runs. To ensure test reliability, use stable fields with known values, such as "name" or "type", in your SearchCriteria.


@Test
void shouldBuildSpecificationForBetweenOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.BETWEEN, "id", List.of("1", "5"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal", "Bird", "Fish");
}

@Test
void shouldBuildSpecificationForINOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.IN, "type", List.of("Mammal", "Bird"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal", "Bird");
}

@Test
void shouldBuildSpecificationForNOTINOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.NOT_IN, "type", List.of("Fish", "Reptile"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal", "Bird");
}

@Test
void shouldBuildSpecificationForLIKEOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.LIKE, "name", List.of("%Lion%"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal");
}

@Test
void shouldBuildSpecificationForCONTAINSOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.CONTAINS, "name", List.of("ar"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Fish");
}

@Test
void shouldBuildSpecificationForSTARTS_WITHOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.STARTS_WITH, "name", List.of("E"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Bird");
}

@Test
void shouldBuildSpecificationForENDS_WITHOperator() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.ENDS_WITH, "name", List.of("e"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteria), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Bird");
}

@Test
Expand All @@ -126,12 +171,25 @@ void shouldBuildSpecificationForANDOperator() {
Specification<Animal> spec =
entitySpecification.specificationBuilder(List.of(criteria1, criteriaAnd, criteria2), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal");
}
Comment on lines +174 to +175
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct the expected result in shouldBuildSpecificationForANDOperator

In the disabled test shouldBuildSpecificationForANDOperator, the assertion expects "Mammal" but the search criteria specify "Bird" with habitat "Forest". The assertion should expect "Bird" to match the criteria.

Apply this diff to correct the assertion:

- assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal");
+ assertThat(results).isNotEmpty().extracting("type").containsOnly("Bird");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Mammal");
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Bird");


@Test
void shouldBuildSpecificationForOROperator() {
SearchCriteria criteriaOr = new SearchCriteria(QueryOperator.OR, "type", List.of("Amphibian", "Fish"));
Specification<Animal> spec = entitySpecification.specificationBuilder(List.of(criteriaOr), Animal.class);
assertThat(spec).isNotNull();
List<Animal> results = animalRepository.findAll(spec);
assertThat(results).isNotEmpty().extracting("type").containsOnly("Fish");
}

@Test
void shouldThrowExceptionForInvalidFieldName() {
SearchCriteria criteria = new SearchCriteria(QueryOperator.EQ, "invalidField", List.of("value"));
assertThatThrownBy(() -> entitySpecification.specificationBuilder(List.of(criteria), Animal.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining(
"Unable to locate Attribute with the given name [invalidField] on this ManagedType [com.example.keysetpagination.entities.Animal]");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.keysetpagination;
package com.example.keysetpagination.repository;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ void shouldReturnResultForNotEqualType() throws Exception {
]
"""))
.andExpect(status().isOk())
// Total animals (10) - Mammals (3) - Birds (3) = 4 animals
.andExpect(jsonPath("$.content", hasSize(4)))
.andExpect(jsonPath("$.last", is(true)));
}
Expand All @@ -173,6 +174,7 @@ void shouldReturnEmptyResultForNonExistentType() throws Exception {
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(0)))
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.last", is(true)));
}

Expand Down
Loading