From cf3c69601355d2e49b05eed86b1620cc3c415e88 Mon Sep 17 00:00:00 2001 From: Jonathan Henrique Medeiros Date: Thu, 27 Jun 2024 16:02:35 -0300 Subject: [PATCH] feature: integration tests example with extensions --- pom.xml | 2 +- .../multidatasources/model/Billionaire.java | 15 ++- .../V0001__inialize-database-schema.sql | 2 +- .../resources/db/test-data/afterMigrate.sql | 8 +- .../CleanupDatabaseExtension.java | 35 ++++++ .../DatabaseRepositoryIntegrationTest.java | 36 ++++++ .../multidatasources/DefaultBillionaire.java | 19 ++++ ...BillionaireParameterResolverExtension.java | 40 +++++++ .../com/multidatasources/IntegrationTest.java | 31 +++++ .../BillionaireRepositoryIntegrationTest.java | 70 ++++++++++++ .../v1/BillionaireServiceIntegrationTest.java | 106 ++++++++++++++++++ .../application-integration-test.yaml | 33 ++++++ src/test/resources/application-test.yaml | 21 ---- 13 files changed, 389 insertions(+), 29 deletions(-) create mode 100644 src/test/java/br/com/multidatasources/CleanupDatabaseExtension.java create mode 100644 src/test/java/br/com/multidatasources/DatabaseRepositoryIntegrationTest.java create mode 100644 src/test/java/br/com/multidatasources/DefaultBillionaire.java create mode 100644 src/test/java/br/com/multidatasources/DefaultBillionaireParameterResolverExtension.java create mode 100644 src/test/java/br/com/multidatasources/IntegrationTest.java create mode 100644 src/test/java/br/com/multidatasources/repository/BillionaireRepositoryIntegrationTest.java create mode 100644 src/test/java/br/com/multidatasources/service/v1/BillionaireServiceIntegrationTest.java create mode 100644 src/test/resources/application-integration-test.yaml delete mode 100644 src/test/resources/application-test.yaml diff --git a/pom.xml b/pom.xml index 552e2c0..4d7372d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.0 + 3.3.1 diff --git a/src/main/java/br/com/multidatasources/model/Billionaire.java b/src/main/java/br/com/multidatasources/model/Billionaire.java index 371d06e..09f68da 100644 --- a/src/main/java/br/com/multidatasources/model/Billionaire.java +++ b/src/main/java/br/com/multidatasources/model/Billionaire.java @@ -4,11 +4,22 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import java.util.Objects; -@Entity -@Table(name = "billionaire") +@Entity(name = "Billionaire") +@Table( + name = "billionaires", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_billionaires_idempotency_id", + columnNames = { + "idempotency_id" + } + ) + } +) public class Billionaire extends IdempotentEntity { @Column(name = "first_name", nullable = false) diff --git a/src/main/resources/db/migration/V0001__inialize-database-schema.sql b/src/main/resources/db/migration/V0001__inialize-database-schema.sql index c7067fa..f96a3ba 100644 --- a/src/main/resources/db/migration/V0001__inialize-database-schema.sql +++ b/src/main/resources/db/migration/V0001__inialize-database-schema.sql @@ -1,4 +1,4 @@ -CREATE TABLE billionaire ( +CREATE TABLE billionaires ( id INT AUTO_INCREMENT PRIMARY KEY, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, diff --git a/src/main/resources/db/test-data/afterMigrate.sql b/src/main/resources/db/test-data/afterMigrate.sql index 91ed8fc..a97f4ce 100644 --- a/src/main/resources/db/test-data/afterMigrate.sql +++ b/src/main/resources/db/test-data/afterMigrate.sql @@ -1,9 +1,9 @@ SET foreign_key_checks = 0; -TRUNCATE TABLE billionaire; +TRUNCATE TABLE billionaires; SET foreign_key_checks = 1; -INSERT INTO billionaire (first_name, last_name, career, idempotency_id) VALUES ('Aliko', 'Dangote', 'Billionaire Industrialist', UUID()), - ('Bill', 'Gates', 'Billionaire Tech Entrepreneur', UUID()), - ('Folrunsho', 'Alakija', 'Billionaire Oil Magnate', UUID()); +INSERT INTO billionaires (first_name, last_name, career, idempotency_id) VALUES ('Aliko', 'Dangote', 'Billionaire Industrialist', UUID()), + ('Bill', 'Gates', 'Billionaire Tech Entrepreneur', UUID()), + ('Folrunsho', 'Alakija', 'Billionaire Oil Magnate', UUID()); diff --git a/src/test/java/br/com/multidatasources/CleanupDatabaseExtension.java b/src/test/java/br/com/multidatasources/CleanupDatabaseExtension.java new file mode 100644 index 0000000..553afed --- /dev/null +++ b/src/test/java/br/com/multidatasources/CleanupDatabaseExtension.java @@ -0,0 +1,35 @@ +package br.com.multidatasources; + +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Map; + +@ActiveProfiles("integration-test") +public class CleanupDatabaseExtension implements BeforeEachCallback { + + @Override + public void beforeEach(final ExtensionContext context) { + final var applicationContext = SpringExtension.getApplicationContext(context); + final var repositoryBeans = applicationContext.getBeansWithAnnotation(Repository.class); + cleanupDatabase(repositoryBeans); + } + + private static void cleanupDatabase(final Map repositoryBeans) { + repositoryBeans.values().forEach(CleanupDatabaseExtension::deleteAll); + } + + private static void deleteAll(final Object repository) { + switch (repository) { + case JpaRepository jpaRepository -> jpaRepository.deleteAllInBatch(); + case CrudRepository crudRepository -> crudRepository.deleteAll(); + default -> throw new IllegalArgumentException("Unsupported repository type: " + repository.getClass()); + } + } + +} diff --git a/src/test/java/br/com/multidatasources/DatabaseRepositoryIntegrationTest.java b/src/test/java/br/com/multidatasources/DatabaseRepositoryIntegrationTest.java new file mode 100644 index 0000000..416be5b --- /dev/null +++ b/src/test/java/br/com/multidatasources/DatabaseRepositoryIntegrationTest.java @@ -0,0 +1,36 @@ +package br.com.multidatasources; + +import br.com.multidatasources.config.datasource.DataSourceRoutingConfiguration; +import br.com.multidatasources.config.datasource.master.MasterDataSourceConfiguration; +import br.com.multidatasources.config.datasource.replica.ReplicaDataSourceConfiguration; +import br.com.multidatasources.config.properties.datasource.DataSourceConnectionPropertiesConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@ActiveProfiles("integration-test") +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ExtendWith(CleanupDatabaseExtension.class) +@Import( + value = { + MasterDataSourceConfiguration.class, + ReplicaDataSourceConfiguration.class, + DataSourceRoutingConfiguration.class, + DataSourceConnectionPropertiesConfiguration.class + } +) +public @interface DatabaseRepositoryIntegrationTest { + +} diff --git a/src/test/java/br/com/multidatasources/DefaultBillionaire.java b/src/test/java/br/com/multidatasources/DefaultBillionaire.java new file mode 100644 index 0000000..070409e --- /dev/null +++ b/src/test/java/br/com/multidatasources/DefaultBillionaire.java @@ -0,0 +1,19 @@ +package br.com.multidatasources; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.UUID; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface DefaultBillionaire { + + String firstName() default "John"; + + String lastName() default "Doe"; + + String career() default "SWE"; + +} diff --git a/src/test/java/br/com/multidatasources/DefaultBillionaireParameterResolverExtension.java b/src/test/java/br/com/multidatasources/DefaultBillionaireParameterResolverExtension.java new file mode 100644 index 0000000..3c5f32d --- /dev/null +++ b/src/test/java/br/com/multidatasources/DefaultBillionaireParameterResolverExtension.java @@ -0,0 +1,40 @@ +package br.com.multidatasources; + +import br.com.multidatasources.model.Billionaire; +import br.com.multidatasources.model.factory.BillionaireBuilder; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +import java.lang.reflect.Parameter; + +public class DefaultBillionaireParameterResolverExtension implements ParameterResolver { + + @Override + public boolean supportsParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException { + return parameterContext.isAnnotated(DefaultBillionaire.class); + } + + @Override + public Object resolveParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException { + return defaultBillionaire(parameterContext.getParameter()); + } + + private static Billionaire defaultBillionaire(final Parameter parameter) { + if (parameter.getType() != Billionaire.class) { + return null; + } + + final var firstName = parameter.getAnnotation(DefaultBillionaire.class).firstName(); + final var lastName = parameter.getAnnotation(DefaultBillionaire.class).lastName(); + final var career = parameter.getAnnotation(DefaultBillionaire.class).career(); + + return BillionaireBuilder.builder() + .firstName(firstName) + .lastName(lastName) + .career(career) + .build(); + } + +} diff --git a/src/test/java/br/com/multidatasources/IntegrationTest.java b/src/test/java/br/com/multidatasources/IntegrationTest.java new file mode 100644 index 0000000..a4b3153 --- /dev/null +++ b/src/test/java/br/com/multidatasources/IntegrationTest.java @@ -0,0 +1,31 @@ +package br.com.multidatasources; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.core.annotation.AliasFor; +import org.springframework.test.context.ActiveProfiles; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@ActiveProfiles("integration-test") +@SpringBootTest( + webEnvironment = WebEnvironment.NONE +) +@ExtendWith(CleanupDatabaseExtension.class) +public @interface IntegrationTest { + + @AliasFor(annotation = SpringBootTest.class, attribute = "classes") + Class[] classes() default {}; + + @AliasFor(annotation = SpringBootTest.class, attribute = "properties") + String[] properties() default {}; + +} diff --git a/src/test/java/br/com/multidatasources/repository/BillionaireRepositoryIntegrationTest.java b/src/test/java/br/com/multidatasources/repository/BillionaireRepositoryIntegrationTest.java new file mode 100644 index 0000000..fd5e456 --- /dev/null +++ b/src/test/java/br/com/multidatasources/repository/BillionaireRepositoryIntegrationTest.java @@ -0,0 +1,70 @@ +package br.com.multidatasources.repository; + +import br.com.multidatasources.DatabaseRepositoryIntegrationTest; +import br.com.multidatasources.model.factory.BillionaireBuilder; +import br.com.multidatasources.service.v1.idempotency.impl.UUIDIdempotencyGenerator; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.UUID; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; + +@DatabaseRepositoryIntegrationTest +class BillionaireRepositoryIntegrationTest { + + @Autowired + private BillionaireRepository billionaireRepository; + + @Test + void givenExistentIdempotencyId_whenExistsBillionaireByIdempotencyId_thenReturnTrue() { + final var idempotencyGenerator = new UUIDIdempotencyGenerator(); + final var billionaire = BillionaireBuilder.builder() + .firstName("John") + .lastName("Doe") + .career("SWE") + .build(); + + billionaire.generateIdempotencyId(idempotencyGenerator); + this.billionaireRepository.save(billionaire); + + final var actual = this.billionaireRepository.existsBillionaireByIdempotencyId(billionaire.getIdempotencyId()); + + assertThat(actual).isTrue(); + } + + @Test + void givenANonExistentIdempotencyId_whenExistsBillionaireByIdempotencyId_thenReturnFalse() { + final var actual = this.billionaireRepository.existsBillionaireByIdempotencyId(UUID.randomUUID()); + assertThat(actual).isFalse(); + } + + @Test + void validateDeleteAllByCallbackExtension() { + // Should be empty table on database + assertThat(this.billionaireRepository.count()).isZero(); + + final var idempotencyGenerator = new UUIDIdempotencyGenerator(); + + // Generate 10 billionaires with random data + final var billionaires = IntStream.range(1, 11) + .mapToObj(index -> { + final var billionaire = BillionaireBuilder.builder() + .firstName(RandomStringUtils.randomAlphabetic(index)) + .lastName(RandomStringUtils.randomAlphabetic(index)) + .career(RandomStringUtils.randomAlphabetic(index)) + .build(); + billionaire.generateIdempotencyId(idempotencyGenerator); + return billionaire; + }).toList(); + + // Save all billionaires + this.billionaireRepository.saveAllAndFlush(billionaires); + + // Should have 10 billionaires on database + assertThat(this.billionaireRepository.count()).isEqualTo(10); + } + +} diff --git a/src/test/java/br/com/multidatasources/service/v1/BillionaireServiceIntegrationTest.java b/src/test/java/br/com/multidatasources/service/v1/BillionaireServiceIntegrationTest.java new file mode 100644 index 0000000..d243117 --- /dev/null +++ b/src/test/java/br/com/multidatasources/service/v1/BillionaireServiceIntegrationTest.java @@ -0,0 +1,106 @@ +package br.com.multidatasources.service.v1; + +import br.com.multidatasources.DefaultBillionaire; +import br.com.multidatasources.DefaultBillionaireParameterResolverExtension; +import br.com.multidatasources.IntegrationTest; +import br.com.multidatasources.model.Billionaire; +import br.com.multidatasources.repository.BillionaireRepository; +import br.com.multidatasources.service.v1.idempotency.IdempotencyGenerator; +import br.com.multidatasources.service.v1.idempotency.impl.UUIDIdempotencyGenerator; +import jakarta.persistence.EntityNotFoundException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; + +@IntegrationTest +@ExtendWith(DefaultBillionaireParameterResolverExtension.class) +class BillionaireServiceIntegrationTest { + + @SpyBean + private IdempotencyGenerator idempotencyGenerator; + + @Autowired + @SpyBean + private BillionaireRepository billionaireRepository; + + @Autowired + private BillionaireService billionaireService; + + @Test + void givenAValidBillionaireId_whenFindBillionaireById_thenReturnASameBillionaireInformed(@DefaultBillionaire final Billionaire billionaire) { + final var localIdempotencyGenerator = new UUIDIdempotencyGenerator(); + billionaire.generateIdempotencyId(localIdempotencyGenerator); + final var persistedBillionaire = this.billionaireRepository.saveAndFlush(billionaire); + + final var actual = this.billionaireService.findById(persistedBillionaire.getId()); + + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(persistedBillionaire); + + verify(this.billionaireRepository).findById(persistedBillionaire.getId()); + } + + @Test + void givenAInvalidBillionaireId_whenFindBillionaireById_thenThrowEntityNotFoundException() { + final var invalidBillionaireId = 0L; + + assertThatThrownBy(() -> this.billionaireService.findById(invalidBillionaireId)) + .isInstanceOf(EntityNotFoundException.class) + .hasMessage("Register with id 0 not found"); + + verify(this.billionaireRepository).findById(invalidBillionaireId); + } + + @Test + void givenATwoBillionaires_whenFindAll_thenReturnListWithTwoRegistries( + @DefaultBillionaire final Billionaire billionaireOne, + @DefaultBillionaire(firstName = "Jake") final Billionaire billionaireTwo + ) { + final var localIdempotencyGenerator = new UUIDIdempotencyGenerator(); + billionaireOne.generateIdempotencyId(localIdempotencyGenerator); + billionaireTwo.generateIdempotencyId(localIdempotencyGenerator); + this.billionaireRepository.saveAllAndFlush(List.of(billionaireOne, billionaireTwo)); + + final var actual = this.billionaireService.findAll(); + + assertThat(actual) + .hasSize(2) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(billionaireOne, billionaireTwo); + + verify(this.billionaireRepository).findAll(); + } + + @Test + void givenEmptyDataBillionaires_whenFindAll_thenReturnListWithZeroRegistries() { + final var actual = this.billionaireService.findAll(); + assertThat(actual).isEmpty(); + verify(this.billionaireRepository).findAll(); + } + + @Test + void givenANewBillionaire_whenSave_thenReturnASameBillionaireSaved(@DefaultBillionaire final Billionaire billionaire) { + final var localIdempotencyGenerator = new UUIDIdempotencyGenerator(); + billionaire.generateIdempotencyId(localIdempotencyGenerator); + + final var actual = this.billionaireService.save(billionaire); + + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(billionaire); + + verify(this.idempotencyGenerator).generate(billionaire); + verify(this.billionaireRepository).existsBillionaireByIdempotencyId(billionaire.getIdempotencyId()); + verify(this.billionaireRepository).save(billionaire); + } + +} diff --git a/src/test/resources/application-integration-test.yaml b/src/test/resources/application-integration-test.yaml new file mode 100644 index 0000000..3857b3c --- /dev/null +++ b/src/test/resources/application-integration-test.yaml @@ -0,0 +1,33 @@ +# DATABASE SCHEMA PROPERTIES +schema: + name: master-db + user: sa + pass: sa + +# SPRING CONFIGURATION +spring: + # SPRING DATA JPA CONFIGURATION + datasource: + driver-class-name: org.h2.Driver + jpa: + show-sql: true + hibernate: + ddl-auto: create-drop + properties: + "[hibernate.format_sql]": true + +# DATABASE MASTER PROPERTIES +master: + datasource: + host: localhost + url: jdbc:h2:mem:${schema.name};MODE=MYSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1 + username: ${schema.user} + password: ${schema.pass} + +# DATABASE REPLICA PROPERTIES +replica: + datasource: + host: localhost + url: jdbc:h2:mem:${schema.name};MODE=MYSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1 + username: ${schema.user} + password: ${schema.pass} diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml deleted file mode 100644 index 740c9d0..0000000 --- a/src/test/resources/application-test.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# DATABASE SCHEMA PROPERTIES -schema: - name: master-db - user: sa - pass: sa - -# DATABASE MASTER PROPERTIES -master: - datasource: - host: localhost - url: jdbc:h2:mem:${schema.name};MODE=MYSQL;DB_CLOSE_DELAY=-1 - username: ${schema.user} - password: ${schema.pass} - -# DATABASE REPLICA PROPERTIES -replica: - datasource: - host: localhost - url: jdbc:h2:mem:${schema.name};MODE=MYSQL;DB_CLOSE_DELAY=-1 - username: ${schema.user} - password: ${schema.pass}