Skip to content

Commit ab46d06

Browse files
author
Jonathan Henrique Medeiros
committed
feature: integration tests example with extensions
1 parent 9541aef commit ab46d06

13 files changed

+368
-29
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>org.springframework.boot</groupId>
77
<artifactId>spring-boot-starter-parent</artifactId>
8-
<version>3.3.0</version>
8+
<version>3.3.1</version>
99
<relativePath/>
1010
</parent>
1111

src/main/java/br/com/multidatasources/model/Billionaire.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@
44
import jakarta.persistence.Column;
55
import jakarta.persistence.Entity;
66
import jakarta.persistence.Table;
7+
import jakarta.persistence.UniqueConstraint;
78

89
import java.util.Objects;
910

10-
@Entity
11-
@Table(name = "billionaire")
11+
@Entity(name = "Billionaire")
12+
@Table(
13+
name = "billionaires",
14+
uniqueConstraints = {
15+
@UniqueConstraint(
16+
name = "uk_billionaires_idempotency_id",
17+
columnNames = {
18+
"idempotency_id"
19+
}
20+
)
21+
}
22+
)
1223
public class Billionaire extends IdempotentEntity {
1324

1425
@Column(name = "first_name", nullable = false)

src/main/resources/db/migration/V0001__inialize-database-schema.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
CREATE TABLE billionaire (
1+
CREATE TABLE billionaires (
22
id INT AUTO_INCREMENT PRIMARY KEY,
33
first_name VARCHAR(255) NOT NULL,
44
last_name VARCHAR(255) NOT NULL,
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
SET foreign_key_checks = 0;
22

3-
TRUNCATE TABLE billionaire;
3+
TRUNCATE TABLE billionaires;
44

55
SET foreign_key_checks = 1;
66

7-
INSERT INTO billionaire (first_name, last_name, career, idempotency_id) VALUES ('Aliko', 'Dangote', 'Billionaire Industrialist', UUID()),
8-
('Bill', 'Gates', 'Billionaire Tech Entrepreneur', UUID()),
9-
('Folrunsho', 'Alakija', 'Billionaire Oil Magnate', UUID());
7+
INSERT INTO billionaires (first_name, last_name, career, idempotency_id) VALUES ('Aliko', 'Dangote', 'Billionaire Industrialist', UUID()),
8+
('Bill', 'Gates', 'Billionaire Tech Entrepreneur', UUID()),
9+
('Folrunsho', 'Alakija', 'Billionaire Oil Magnate', UUID());
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package br.com.multidatasources;
2+
3+
import org.junit.jupiter.api.extension.BeforeEachCallback;
4+
import org.junit.jupiter.api.extension.ExtensionContext;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.repository.CrudRepository;
7+
import org.springframework.stereotype.Repository;
8+
import org.springframework.test.context.ActiveProfiles;
9+
import org.springframework.test.context.junit.jupiter.SpringExtension;
10+
11+
import java.util.Map;
12+
13+
@ActiveProfiles("integration-test")
14+
public class CleanupDatabaseExtension implements BeforeEachCallback {
15+
16+
@Override
17+
public void beforeEach(final ExtensionContext context) {
18+
final var applicationContext = SpringExtension.getApplicationContext(context);
19+
final var repositoryBeans = applicationContext.getBeansWithAnnotation(Repository.class);
20+
cleanupDatabase(repositoryBeans);
21+
}
22+
23+
private static void cleanupDatabase(final Map<String, Object> repositoryBeans) {
24+
repositoryBeans.values().forEach(CleanupDatabaseExtension::deleteAll);
25+
}
26+
27+
private static void deleteAll(final Object repository) {
28+
switch (repository) {
29+
case JpaRepository<?, ?> jpaRepository -> jpaRepository.deleteAllInBatch();
30+
case CrudRepository<?, ?> crudRepository -> crudRepository.deleteAll();
31+
default -> throw new IllegalArgumentException("Unsupported repository type: " + repository.getClass());
32+
}
33+
}
34+
35+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package br.com.multidatasources;
2+
3+
import br.com.multidatasources.config.datasource.DataSourceRoutingConfiguration;
4+
import br.com.multidatasources.config.datasource.master.MasterDataSourceConfiguration;
5+
import br.com.multidatasources.config.datasource.replica.ReplicaDataSourceConfiguration;
6+
import br.com.multidatasources.config.properties.datasource.DataSourceConnectionPropertiesConfiguration;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
9+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
10+
import org.springframework.context.annotation.Import;
11+
import org.springframework.test.context.ActiveProfiles;
12+
13+
import java.lang.annotation.ElementType;
14+
import java.lang.annotation.Inherited;
15+
import java.lang.annotation.Retention;
16+
import java.lang.annotation.RetentionPolicy;
17+
import java.lang.annotation.Target;
18+
19+
@Target(ElementType.TYPE)
20+
@Retention(RetentionPolicy.RUNTIME)
21+
@Inherited
22+
@ActiveProfiles("integration-test")
23+
@DataJpaTest
24+
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
25+
@ExtendWith(CleanupDatabaseExtension.class)
26+
@Import(
27+
value = {
28+
MasterDataSourceConfiguration.class,
29+
ReplicaDataSourceConfiguration.class,
30+
DataSourceRoutingConfiguration.class,
31+
DataSourceConnectionPropertiesConfiguration.class
32+
}
33+
)
34+
public @interface DatabaseRepositoryIntegrationTest {
35+
36+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package br.com.multidatasources;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
import java.util.UUID;
8+
9+
@Retention(RetentionPolicy.RUNTIME)
10+
@Target(ElementType.PARAMETER)
11+
public @interface DefaultBillionaire {
12+
13+
String firstName() default "John";
14+
15+
String lastName() default "Doe";
16+
17+
String career() default "SWE";
18+
19+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package br.com.multidatasources;
2+
3+
import br.com.multidatasources.model.Billionaire;
4+
import br.com.multidatasources.model.factory.BillionaireBuilder;
5+
import org.junit.jupiter.api.extension.ExtensionContext;
6+
import org.junit.jupiter.api.extension.ParameterContext;
7+
import org.junit.jupiter.api.extension.ParameterResolutionException;
8+
import org.junit.jupiter.api.extension.ParameterResolver;
9+
10+
import java.lang.reflect.Parameter;
11+
import java.util.Optional;
12+
13+
public class DefaultBillionaireParameterResolverExtension implements ParameterResolver {
14+
15+
@Override
16+
public boolean supportsParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException {
17+
return parameterContext.isAnnotated(DefaultBillionaire.class);
18+
}
19+
20+
@Override
21+
public Object resolveParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException {
22+
return defaultBillionaire(parameterContext.getParameter());
23+
}
24+
25+
private static Billionaire defaultBillionaire(final Parameter parameter) {
26+
if (parameter.getType() != Billionaire.class) {
27+
throw new IllegalArgumentException("Parameter must be of type Billionaire");
28+
}
29+
30+
final var firstName = Optional.ofNullable(parameter.getAnnotation(DefaultBillionaire.class))
31+
.map(DefaultBillionaire::firstName)
32+
.orElse("John");
33+
final var lastName = Optional.ofNullable(parameter.getAnnotation(DefaultBillionaire.class))
34+
.map(DefaultBillionaire::lastName)
35+
.orElse("Doe");
36+
final var career = Optional.ofNullable(parameter.getAnnotation(DefaultBillionaire.class))
37+
.map(DefaultBillionaire::career)
38+
.orElse("Doe");
39+
40+
return BillionaireBuilder.builder()
41+
.firstName(firstName)
42+
.lastName(lastName)
43+
.career(career)
44+
.build();
45+
}
46+
47+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package br.com.multidatasources;
2+
3+
import org.junit.jupiter.api.extension.ExtendWith;
4+
import org.springframework.boot.test.context.SpringBootTest;
5+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
6+
import org.springframework.core.annotation.AliasFor;
7+
import org.springframework.test.context.ActiveProfiles;
8+
9+
import java.lang.annotation.ElementType;
10+
import java.lang.annotation.Inherited;
11+
import java.lang.annotation.Retention;
12+
import java.lang.annotation.RetentionPolicy;
13+
import java.lang.annotation.Target;
14+
15+
@Target(ElementType.TYPE)
16+
@Retention(RetentionPolicy.RUNTIME)
17+
@Inherited
18+
@ActiveProfiles("integration-test")
19+
@SpringBootTest(
20+
webEnvironment = WebEnvironment.NONE
21+
)
22+
@ExtendWith(CleanupDatabaseExtension.class)
23+
public @interface IntegrationTest {
24+
25+
@AliasFor(annotation = SpringBootTest.class, attribute = "classes")
26+
Class<?>[] classes() default {};
27+
28+
@AliasFor(annotation = SpringBootTest.class, attribute = "properties")
29+
String[] properties() default {};
30+
31+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package br.com.multidatasources.repository;
2+
3+
import br.com.multidatasources.DatabaseRepositoryIntegrationTest;
4+
import br.com.multidatasources.model.factory.BillionaireBuilder;
5+
import br.com.multidatasources.service.v1.idempotency.impl.UUIDIdempotencyGenerator;
6+
import org.junit.jupiter.api.Test;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
9+
import java.util.UUID;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
@DatabaseRepositoryIntegrationTest
14+
class BillionaireRepositoryIntegrationTest {
15+
16+
@Autowired
17+
private BillionaireRepository billionaireRepository;
18+
19+
@Test
20+
void givenExistentIdempotencyId_whenExistsBillionaireByIdempotencyId_thenReturnTrue() {
21+
final var idempotencyGenerator = new UUIDIdempotencyGenerator();
22+
final var billionaire = BillionaireBuilder.builder()
23+
.firstName("John")
24+
.lastName("Doe")
25+
.career("SWE")
26+
.build();
27+
28+
billionaire.generateIdempotencyId(idempotencyGenerator);
29+
this.billionaireRepository.save(billionaire);
30+
31+
final var actual = this.billionaireRepository.existsBillionaireByIdempotencyId(billionaire.getIdempotencyId());
32+
33+
assertThat(actual).isTrue();
34+
}
35+
36+
@Test
37+
void givenANonExistentIdempotencyId_whenExistsBillionaireByIdempotencyId_thenReturnFalse() {
38+
final var actual = this.billionaireRepository.existsBillionaireByIdempotencyId(UUID.randomUUID());
39+
assertThat(actual).isFalse();
40+
}
41+
42+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package br.com.multidatasources.service.v1;
2+
3+
import br.com.multidatasources.DefaultBillionaire;
4+
import br.com.multidatasources.DefaultBillionaireParameterResolverExtension;
5+
import br.com.multidatasources.IntegrationTest;
6+
import br.com.multidatasources.model.Billionaire;
7+
import br.com.multidatasources.repository.BillionaireRepository;
8+
import br.com.multidatasources.service.v1.idempotency.IdempotencyGenerator;
9+
import br.com.multidatasources.service.v1.idempotency.impl.UUIDIdempotencyGenerator;
10+
import jakarta.persistence.EntityNotFoundException;
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.extension.ExtendWith;
13+
import org.springframework.beans.factory.annotation.Autowired;
14+
import org.springframework.boot.test.mock.mockito.SpyBean;
15+
16+
import java.util.List;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
import static org.mockito.Mockito.verify;
21+
22+
@IntegrationTest
23+
@ExtendWith(DefaultBillionaireParameterResolverExtension.class)
24+
class BillionaireServiceIntegrationTest {
25+
26+
@SpyBean
27+
private IdempotencyGenerator idempotencyGenerator;
28+
29+
@Autowired
30+
@SpyBean
31+
private BillionaireRepository billionaireRepository;
32+
33+
@Autowired
34+
private BillionaireService billionaireService;
35+
36+
@Test
37+
void givenAValidBillionaireId_whenFindBillionaireById_thenReturnASameBillionaireInformed(@DefaultBillionaire final Billionaire billionaire) {
38+
final var localIdempotencyGenerator = new UUIDIdempotencyGenerator();
39+
billionaire.generateIdempotencyId(localIdempotencyGenerator);
40+
final var persistedBillionaire = this.billionaireRepository.saveAndFlush(billionaire);
41+
42+
final var actual = this.billionaireService.findById(persistedBillionaire.getId());
43+
44+
assertThat(actual)
45+
.usingRecursiveComparison()
46+
.isEqualTo(persistedBillionaire);
47+
48+
verify(this.billionaireRepository).findById(persistedBillionaire.getId());
49+
}
50+
51+
@Test
52+
void givenAInvalidBillionaireId_whenFindBillionaireById_thenThrowEntityNotFoundException() {
53+
final var invalidBillionaireId = 0L;
54+
55+
assertThatThrownBy(() -> this.billionaireService.findById(invalidBillionaireId))
56+
.isInstanceOf(EntityNotFoundException.class)
57+
.hasMessage("Register with id 0 not found");
58+
59+
verify(this.billionaireRepository).findById(invalidBillionaireId);
60+
}
61+
62+
@Test
63+
void givenATwoBillionaires_whenFindAll_thenReturnListWithTwoRegistries(
64+
@DefaultBillionaire final Billionaire billionaireOne,
65+
@DefaultBillionaire(firstName = "Jake") final Billionaire billionaireTwo
66+
) {
67+
final var localIdempotencyGenerator = new UUIDIdempotencyGenerator();
68+
billionaireOne.generateIdempotencyId(localIdempotencyGenerator);
69+
billionaireTwo.generateIdempotencyId(localIdempotencyGenerator);
70+
this.billionaireRepository.saveAllAndFlush(List.of(billionaireOne, billionaireTwo));
71+
72+
final var actual = this.billionaireService.findAll();
73+
74+
assertThat(actual)
75+
.hasSize(2)
76+
.usingRecursiveFieldByFieldElementComparator()
77+
.containsExactlyInAnyOrder(billionaireOne, billionaireTwo);
78+
79+
verify(this.billionaireRepository).findAll();
80+
}
81+
82+
@Test
83+
void givenEmptyDataBillionaires_whenFindAll_thenReturnListWithZeroRegistries() {
84+
final var actual = this.billionaireService.findAll();
85+
assertThat(actual).isEmpty();
86+
verify(this.billionaireRepository).findAll();
87+
}
88+
89+
@Test
90+
void givenANewBillionaire_whenSave_thenReturnASameBillionaireSaved(@DefaultBillionaire final Billionaire billionaire) {
91+
final var localIdempotencyGenerator = new UUIDIdempotencyGenerator();
92+
billionaire.generateIdempotencyId(localIdempotencyGenerator);
93+
94+
final var actual = this.billionaireService.save(billionaire);
95+
96+
assertThat(actual)
97+
.usingRecursiveComparison()
98+
.ignoringFields("id")
99+
.isEqualTo(billionaire);
100+
101+
verify(this.idempotencyGenerator).generate(billionaire);
102+
verify(this.billionaireRepository).existsBillionaireByIdempotencyId(billionaire.getIdempotencyId());
103+
verify(this.billionaireRepository).save(billionaire);
104+
}
105+
106+
}

0 commit comments

Comments
 (0)