51
loading...
This website collects cookies to deliver better user experience
src/test/resources/application.yml
. It does work but it makes the build hard to reproduce. Every developer that is working on the project has to be sure that they have two separate databases. One for development and another one for tests running. Besides, it makes executing the build on CI/CD environment a real challenge.testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
runtimeOnly 'org.postgresql:postgresql'
src/test/resources
. Spring Boot is able to distinguish different configuration files by the profiles. The profile name should be placed as a suffix like application-PROFILE_NAME.yml
. For example, the configuration file named application-test-containers.yml
is applied only when test-containers
profile is active.spring:
datasource:
url: jdbc:tc:postgresql:9.6.8:///test_database
username: user
password: password
jpa:
hibernate:
ddl-auto: create
tc
suffix in the JDBC-connection string? That’s the magic that comes with the union of JUnit 5 and Testcontainers. The thing is that you don’t need any programmatic configurations at all! When the framework sees that url
contains the tc
suffix it runs all necessary Docker commands internally. You can find more examples here.We set spring.jpa.hibernate.ddl-auto=create
property
so the database schema shall be created automatically
according to definition of entity classes.
Flyway integration is described in the next section.
PersonCreateService.createFamily
method and its H2 test again.@Service
@RequiredArgsConstructor
public class PersonCreateServiceImpl implements PersonCreateService {
private final PersonValidateService personValidateService;
private final PersonRepository personRepository;
@Override
@Transactional
public List<PersonDTO> createFamily(Iterable<String> firstNames, String lastName) {
final var people = new ArrayList<PersonDTO>();
firstNames.forEach(firstName -> people.add(createPerson(firstName, lastName)));
return people;
}
@Override
@Transactional
public PersonDTO createPerson(String firstName, String lastName) {
personValidateService.checkUserCreation(firstName, lastName);
final var createdPerson = personRepository.saveAndFlush(
new Person()
.setFirstName(firstName)
.setLastName(lastName)
);
return DTOConverters.toPersonDTO(createdPerson);
}
}
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureTestDatabase
class PersonCreateServiceImplSpringBootTest {
@Autowired
private PersonRepository personRepository;
@MockBean
private PersonValidateService personValidateService;
@Autowired
private PersonCreateService personCreateService;
@BeforeEach
void init() {
personRepository.deleteAll();
}
@Test
void shouldCreateOnePerson() {
final var people = personCreateService.createFamily(
List.of("Simon"),
"Kirekov"
);
assertEquals(1, people.size());
final var person = people.get(0);
assertEquals("Simon", person.getFirstName());
assertEquals("Kirekov", person.getLastName());
assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
}
@Test
void shouldRollbackIfAnyUserIsNotValidated() {
doThrow(new ValidationFailedException(""))
.when(personValidateService)
.checkUserCreation("John", "Brown");
assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
List.of("Matilda", "Vasya", "John"),
"Brown"
));
assertEquals(0, personRepository.count());
}
}
@AutoConfigureTestDatabase
though should be removed.@ActiveProfiles("test-containers")
— activates the test-containers
profile so the Spring could read the configuration file that was described earlier@Testcontainers
— tells to run PostgreSQL instance in Docker automagically
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@Testcontainers
@ActiveProfiles("test-containers")
class PersonCreateServiceImplTestContainers {
@Autowired
private PersonRepository personRepository;
@MockBean
private PersonValidateService personValidateService;
@Autowired
private PersonCreateService personCreateService;
@BeforeEach
void init() {
personRepository.deleteAll();
}
@Test
void shouldCreateOnePerson() {
final var people = personCreateService.createFamily(
List.of("Simon"),
"Kirekov"
);
assertEquals(1, people.size());
final var person = people.get(0);
assertEquals("Simon", person.getFirstName());
assertEquals("Kirekov", person.getLastName());
assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
}
@Test
void shouldRollbackIfAnyUserIsNotValidated() {
doThrow(new ValidationFailedException(""))
.when(personValidateService)
.checkUserCreation("John", "Brown");
assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
List.of("Matilda", "Vasya", "John"),
"Brown"
));
assertEquals(0, personRepository.count());
}
}
@DataJpaTest
class PersonRepositoryDataJpaTest {
@Autowired
private PersonRepository personRepository;
@Test
void shouldReturnAlLastNames() {
personRepository.saveAndFlush(new Person().setFirstName("John").setLastName("Brown"));
personRepository.saveAndFlush(new Person().setFirstName("Kyle").setLastName("Green"));
personRepository.saveAndFlush(new Person().setFirstName("Paul").setLastName("Brown"));
assertEquals(Set.of("Brown", "Green"), personRepository.findAllLastNames());
}
}
@DataJpaTest
is annotated with @AutoConfigureTestDatabase
itself. This annotation replaces any data source with the H2 instance by default. So, we need to override this behavior by adding replace=Replace.NONE
property.@DataJpaTest
@Testcontainers
@ActiveProfiles("test-containers")
@AutoConfigureTestDatabase(replace = Replace.NONE)
class PersonRepositoryTestContainers {
@Autowired
private PersonRepository personRepository;
@Test
void shouldReturnAlLastNames() {
personRepository.saveAndFlush(new Person().setFirstName("John").setLastName("Brown"));
personRepository.saveAndFlush(new Person().setFirstName("Kyle").setLastName("Green"));
personRepository.saveAndFlush(new Person().setFirstName("Paul").setLastName("Brown"));
assertEquals(Set.of("Brown", "Green"), personRepository.findAllLastNames());
}
}
implementation "org.flywaydb:flyway-core"
application-test-containers.yml
because there will be a separate configuration file.spring:
datasource:
url: jdbc:tc:postgresql:9.6.8:///test_database
username: user
password: password
jpa:
hibernate:
ddl-auto: create
flyway:
enabled: false
application-test-containers-flyway.yml
. The library provides lots of auto-configuration. So, actually, we don’t need to tune anything.spring:
datasource:
url: jdbc:tc:postgresql:9.6.8:///test_database
username: user
password: password
resources/db/migration
.create table person
(
id serial primary key,
first_name text,
last_name text,
date_created timestamp with time zone
);
test-containers
profile with test-containers-flyway
one.@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@Testcontainers
@ActiveProfiles("test-containers-flyway")
class PersonCreateServiceImplTestContainersFlyway {
@Autowired
private PersonRepository personRepository;
@MockBean
private PersonValidateService personValidateService;
@Autowired
private PersonCreateService personCreateService;
@BeforeEach
void init() {
personRepository.deleteAll();
}
@Test
void shouldCreateOnePerson() {
final var people = personCreateService.createFamily(
List.of("Simon"),
"Kirekov"
);
assertEquals(1, people.size());
final var person = people.get(0);
assertEquals("Simon", person.getFirstName());
assertEquals("Kirekov", person.getLastName());
assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
}
@Test
void shouldRollbackIfAnyUserIsNotValidated() {
doThrow(new ValidationFailedException(""))
.when(personValidateService)
.checkUserCreation("John", "Brown");
assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
List.of("Matilda", "Vasya", "John"),
"Brown"
));
assertEquals(0, personRepository.count());
}
}
/var/run/docker.sock
as a volume. More than that, the directory inside the container should be the same as the one where the container was launched.gradle
.docker run -it \
--rm \
-v $PWD:$PWD \
-w $PWD \
-v /var/run/docker.sock:/var/run/docker.sock gradle:7.1-jdk8 \
gradle test