Skip to content

Commit

Permalink
feature: integration tests example with extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonathan Henrique Medeiros committed Jun 27, 2024
1 parent 9541aef commit ab46d06
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 29 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<version>3.3.1</version>
<relativePath/>
</parent>

Expand Down
15 changes: 13 additions & 2 deletions src/main/java/br/com/multidatasources/model/Billionaire.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/main/resources/db/test-data/afterMigrate.sql
Original file line number Diff line number Diff line change
@@ -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());
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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());
}
}

}
Original file line number Diff line number Diff line change
@@ -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 {

}
19 changes: 19 additions & 0 deletions src/test/java/br/com/multidatasources/DefaultBillionaire.java
Original file line number Diff line number Diff line change
@@ -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";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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;
import java.util.Optional;

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) {
throw new IllegalArgumentException("Parameter must be of type Billionaire");
}

final var firstName = Optional.ofNullable(parameter.getAnnotation(DefaultBillionaire.class))
.map(DefaultBillionaire::firstName)
.orElse("John");
final var lastName = Optional.ofNullable(parameter.getAnnotation(DefaultBillionaire.class))
.map(DefaultBillionaire::lastName)
.orElse("Doe");
final var career = Optional.ofNullable(parameter.getAnnotation(DefaultBillionaire.class))
.map(DefaultBillionaire::career)
.orElse("Doe");

return BillionaireBuilder.builder()
.firstName(firstName)
.lastName(lastName)
.career(career)
.build();
}

}
31 changes: 31 additions & 0 deletions src/test/java/br/com/multidatasources/IntegrationTest.java
Original file line number Diff line number Diff line change
@@ -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 {};

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.UUID;

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();
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Loading

0 comments on commit ab46d06

Please sign in to comment.