The question is how do you match database infrastructure of your test environment with the production one?
Most commonly developers use in-memory databases like H2 or HSQLDB. But what if you want to test your code against SAME/REAL DATABASE INFRASTRUCTURE IN PRODUCTION?
Amigoscode stop using H2 in memory-database RIGHT NOW Provides a better explanation in code but let's see
how we can do it in practice.
DOWNSIDES OF IN-MEMORY DATABASES
- Lack of support for some SQL features constrains cause less realistic tests(Reduced reliability)
POSSIBLE SOLUTION TestContainers
You can USE TestContainers library. It allows you to run a real database in a docker container and connect to it from your tests.
How does testContainers work?
TestContainers uses DOCKER API. PORT exposes to the host machine.
- Starts a container with the DB.
- Exposes the PORT of DB.
- Provides and Connect the JDBC URL.
- Container is destroyed after tests are finished.
HOW TO USE IT ?
In a simple maven application we can add the following dependency to our pom.xml file:
Based on the database we are using we can add appropriate infrastructure dependency.
<?xml version="1.0" encoding="UTF-8"?>
<project >
<!--...-->
<properties>
<testcontainers.version>1.17.6</testcontainers.version>
</properties>
<dependencies>
<!--TEST CONTAINERS-->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<!--END TEST CONTAINERS-->
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Lets have a look at how we can test our DAO layer with testContainers.
In this instance we would like to test our
public interface UserEntityRepository extends CrudRepository<UserEntity, String> {
}
We can start our TEST by first removing @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE))the default embedded database for testing
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
public class UserEntityRepositoryTest {
@Container
private static GenericContainer<?> container= new PostgreSQLContainer<PostgreSQLContainer>("postgres:13")
.withExposedPorts(5432)
.withEnv("POSTGRES_USER", "test")
.withEnv("POSTGRES_PASSWORD", "test")
.withEnv("POSTGRES_DB", "test");
@Test
public void givenUserName_canSelectByUsername() {
}
}
The above method is a generic way of starting a container.
We can be more specific by using the following method:
Also we can tell spring to use dynamic properties for the test.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
public class UserEntityRepositoryTest {
@Container
private static PostgreSQLContainer<?> container= new PostgreSQLContainer<PostgreSQLContainer>("postgres:13");
@DynamicPropertySource
static void setUpDB(DynamicPropertyRegistry registry){
registry.add("spring.datasource.url",container::getJdbcUrl );
registry.add("spring.datasource.password", container::getPassword);
registry.add("spring.datasource.username", container::getUsername);
}
@Test
public void givenUserName_canSelectByUsername() {
}
}
If we check the EXTENDED classes(TestcontainersExtension) of the TestContainers we can see that it has a lot of useful methods.That tracks all the JUNIT life cycle
We can follow the logs by running our test class again.
But then a better log view configuration for the test resource is provided from the official site.
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.testcontainers" level="INFO"/>
<!-- The following logger can be used for containers logs since 1.18.0 -->
<logger name="tc" level="INFO"/>
<logger name="com.github.dockerjava" level="WARN"/>
<logger name="com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire" level="OFF"/>
</configuration>
Our test class logs looks like below:
The Test Passed. But then ?
The test passes, but then do we need to create a new container for each test Class?
Our code
@Container
private static PostgreSQLContainer<?> container= new PostgreSQLContainer<>("postgres:13");
@DynamicPropertySource
static void setUpDB(DynamicPropertyRegistry registry){
registry.add("spring.datasource.url",container::getJdbcUrl );
registry.add("spring.datasource.password", container::getPassword);
registry.add("spring.datasource.username", container::getUsername);
}
Would repeat in several classes.
POSSIBLE NOT
We can create an abstract class that would be extended by all the test classes.
@Testcontainers
public abstract class DBAbstractionTest {
@Container
private static PostgreSQLContainer<?> container= new PostgreSQLContainer<>("postgres:13");
@DynamicPropertySource
static void setUpDB(DynamicPropertyRegistry registry){
registry.add("spring.datasource.url",container::getJdbcUrl );
registry.add("spring.datasource.password", container::getPassword);
registry.add("spring.datasource.username", container::getUsername);
}
}
And still our test would pass.
A Better Approach
TestContainers provides a way to reuse the same container for all the test classes.
Which provides a profile for the test classes.
And it has specification for each db infrastructure.
In our case we are using PostgreSQLContainer at src/test/resources/application-postgres.properties
spring.datasource.url=jdbc:tc:postgresql:13:///omayfas
spring.test.database.replace=none
By simply adding these properties with our active profile set to postgres
We still get a passed test.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("postgres")
public class UserEntityRepositoryTest {
@Autowired
UserEntityRepository userEntityRepository;
@Test
@DisplayName("Given USER EXISTS, when findByName, then return UserEntity")
public void givenIfUserExist_canQueryByName() {
userEntityRepository.save(new UserEntity("test", "test", "test@gmail.com", "test", "test.png", LoginProvider.APP));
Optional<UserEntity> test = userEntityRepository.findByName("test");
Assertions.assertThat(test).hasValueSatisfying(ue -> {
Assertions.assertThat(ue.getName()).isEqualTo("test");
Assertions.assertThat(ue.getProvider()).isEqualTo(LoginProvider.APP);
Assertions.assertThat(ue.getUsername()).isNotNull();
});
}
}
ERROR WITH PROFILE
Notice with no profile we get an error.
Because the application test context is not aware of the container.
and fails to resolve the datasource properties for the test classes.
So our application text context should be like.
@SpringBootTest
@ActiveProfiles("postgres")
class OmayfasBlogApplicationTests {
@Test
void contextLoads() {
}
}
## Conclusion
Provides a way to test our application with a real database.
Ability to test our queries and the database itself.
Give it a try.
REFERENCES
'Spring Boot' 카테고리의 다른 글
SER.1. How to Create Multi-Project Maven POMs (3) | 2023.04.05 |
---|---|
How to trigger Mono execution after another Mono terminates (0) | 2023.03.09 |
What is Spring Mobile Device (0) | 2022.11.07 |