How to test repository with junit5 and testcontainers? - spring-boot

I have a sample project in which I experiment with different technologies.
I have the following setup:
Spring Boot 2.3.4.RELEASE
Flyway 7.0.1
Testcontainers 1.15.0-rc2
Junit 5.7.0
How can I test the Repository layer with testcontainer-junit5?
Example of code I have now for CompanyRepositoryTest.java:
#ExtendWith(SpringExtension.class)
#Testcontainers
public class CompanyRepositoryTest {
#Autowired
private CompanyRepository companyRepository;
#Container
public MySQLContainer mysqlContainer = new MySQLContainer()
.withDatabaseName("foo")
.withUsername("foo")
.withPassword("secret");;
#Test
public void whenFindByIdExecuted_thenNullReturned()
throws Exception {
assertEquals(companyRepository.findById(1L), Optional.ofNullable(null));
}
#Test
public void whenFindAllExecuted_thenEmptyListReturned() {
assertEquals(companyRepository.findAll(), new ArrayList<>());
}
}
When I add #SpringBootTest, I need to set up all the context and have some Application load context issues?
The question is, can anyone demystify what #TestContainers annotation does? What is the best practice or correct to use it while testing the Repository?

The JUnit 5 extension provided by the #Testcontainers annotation scans for any containers declared with the #Container annotation, and then starts and stops the those containers for your tests. Containers as static fields will be shared with all tests, and containers as instance fields will be started and stopped for every test.
If you are using Spring Boot, the easiest way to setup testcontainers for your tests is probably to provide properties in application-test.yml. This will use the datasource JDBC URL to launch the testcontainers container. Refer to Testcontainers JDBC support for more information.
You can also test just the repository layer by using #DataJpaTest instead of #SpringBootTest:
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
#ActiveProfiles("test")
class CompanyRepositoryTest { }
Your application-test.yml file:
spring:
datasource:
url: jdbc:tc:mysql:8.0://hostname/databasename
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
In some cases you might also want to use the #TestPropertySource annotation instead:
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
#TestPropertySource(
properties = {
"spring.datasource.url = jdbc:tc:mysql:8.0://hostname/test-database",
"spring.datasource.driver-class-name = org.testcontainers.jdbc.ContainerDatabaseDriver"
}
)
class CompanyRepositoryTest { }
Please note that the hostname and test-database are not actually used anywhere.

You said
When I add #SpringBootTest, I need to set up all the context and have
some Application load context issues?
If you'd like to try an alternative and Testcontainer is not mandatory you can do it differently.
You do not need to load everyting when using SpringBootTest annotation, you can specify which classes are needed such as
#SpringBootTest(classes = { TheService.class })
or use #Import annotation
and mock others such as
#MockBean
MyService service;
For database connection you can use annotation such as
#ActiveProfiles("my-profile-for-jpa-test")
#DataJpaTest
#EnableJpaAuditing
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
EDIT: I feel like this should be an comment but I wanted to address the SpringBootTest part of the question with proper formatting

Here is an example, how I configured Liquibase (a similar framework to Flyway) with MySql inside Spring:
#DataJpaTest
#TestPropertySource(properties = {"spring.jpa.hibernate.ddl-auto=validate"})
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
#ContextConfiguration(initializers = { MySqlLiquibaseBaseIT.Initializer.class })
#Testcontainers
public class MySqlLiquibaseBaseIT {
#Container
public static MySQLContainer<?> mysql = new MySQLContainer<>(
DockerImageName
.parse(MySQLContainer.NAME)
.withTag("5.7.22"));
#Configuration
#EnableJpaRepositories
#EntityScan
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=" + mysql.getJdbcUrl(),
"spring.datasource.username=" + mysql.getUsername(),
"spring.datasource.password=" + mysql.getPassword(),
"spring.datasource.driver-class-name=" + mysql.getDriverClassName())
.applyTo(configurableApplicationContext.getEnvironment());
}
#Bean
public SpringLiquibase springLiquibase(DataSource dataSource) {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setDropFirst(true);
liquibase.setDataSource(dataSource);
liquibase.setChangeLog("classpath:/db/changelog/db.changelog-master.yml");
return liquibase;
}
}
}
Full MySqlLiquibaseBaseIT.java

As per docs:
The test containers extension finds all fields that are annotated with
Container and calls their container lifecycle methods. Containers
declared as static fields will be shared between test methods. They
will be started only once before any test method is executed and
stopped after the last test method has executed. Containers declared
as instance fields will be started and stopped for every test method.
So in your case it will recreate a container for every test method, it's only responsible for starting and stopping the container. If you need some test data - that has to be done manually, as I see you have Flyway, that should do.
What "context issues" are you talking about?
Repositories are usually not tested separately, you can just test services which run repository methods instead of writing tests for both. If you want to test repos anyway - fill the database with some data in #Before.
If you have more questions please ask.

Related

PostConstruct and test

I use spring boot 3
Main spring boot class
#EnableTransactionManagement
#SpringBootApplication
#Slf4j
public class FlexApplication{
private final ApplicationParameterManager appParamManager;
public FlexApplication(ApplicationParameterManager appParamManager) {
this.appParamManager = appParamManager;
}
#PostConstruct
public void init(){
}
....
}
#Service
#Slf4j
public class ApplicationParameterManager{
....
}
Basic test
#AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)
#DataJpaTest
public class ListUserRepositoryTest {
#Autowired
private ListUserRepository repository;
#Test
public void getListUserByUserType(){
String typeUser = "CETEST";
Pageable page = Pageable.ofSize(10);
Page<ListUser> pageListUser = repository.findAllByTypeUser(typeUser, page);
assertThat(pageListUser.getContent().size() > 5 ).isTrue();
}
}
Otherwise this test, application run well
I get this error
Parameter 0 of constructor in com.acme.FlexApplication required a bean
of type 'com.acme.parameter.ApplicationParameterManager' that could
not be found.
I think it is not related to version of Spring Boot.
As you're using #DataJpaTest , your bean is not created
Spring Docs:
#DataJpaTest can be used if you want to test JPA applications. By
default it will configure an in-memory embedded database, scan for
#Entity classes and configure Spring Data JPA repositories. Regular
#Component beans will not be loaded into the ApplicationContext.
Solution would be to use #SpringBootTest instead of #DataJpaTest if your test is not really a JPA test.
Also, still using #DataJpaTest you could add #Import(ApplicationParameterManager.class) to your test class
When using #DataJpaTest, you are not creating the whole spring context as when you run the application normally but you only create the beans responsible for data access layer.
In order to run your tests properly, you need to provide a mocked bean of type ApplicationParameterManager.
The easiest way to do it is by utilizing #MockBean annotation.
So, to make your tests work, edit the test in the following way.
#AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)
#DataJpaTest
public class ListUserRepositoryTest {
#MockBean
private ApplicationParameterManager applicationParameterManager;
#Autowired
private ListUserRepository repository;
#Test
public void getListUserByUserType(){
String typeUser = "CETEST";
Pageable page = Pageable.ofSize(10);
Page<ListUser> pageListUser = repository.findAllByTypeUser(typeUser, page);
assertThat(pageListUser.getContent().size() > 5 ).isTrue();
}
}
That way, the spring context will include a mocked bean of your required dependency.
Take a look at #MockBean java doc for more information. https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/mock/mockito/MockBean.html
If you prefer to run the whole spring context in order to perform full integration tests, take a look at #SpringBootTest annotation.
#DataJpaTest should be used when you want to test data access layer in isolation.

How to exclude Mongo spring data configuration only for tests

I am doing a simple sample project with Spring boot and data.
#Configuration
public class MongoConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(MongoConfig.class);
#Value("${mongo.uri}")
private String mongoUri;
public #Bean MongoClient mongoClient() {
LOGGER.info(" creating connection with mongodb with uri [{}] ", mongoUri);
return MongoClients.create(mongoUri);
}
}
This works fine and connects to mongo on startup. However, the tests also pick this up in autoscan. What is the best practice to make sure that mongo config gets excluded for tests?
If I add #WebMvcTest to the tests, it works. But not all tests will be mvc tests. I might be testing a utility class.
If I try using profiles, it gives my an error java.lang.IllegalStateException: The following classes could not be excluded because they are not auto-configuration classes: ...MongoConfig
#SpringBootTest
#ActiveProfiles("test")
class ApplicationTests {
#Test
void contextLoads() {
}
}
Please tell me a repeatable practice as I will be using it for all my tests.
The simplest way of doing it is to exclude your configuration bean when your test profile is active
#Profile("!test")
#Configuration
public class MongoConfig {
...
}
Here you tell to the BeanFactory to not create this bean if the profile test is present
Ref: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Profile.html

How to inject values from application.yaml to Junit 5 Test case

I am using Junit5 to write unit test cases in java. I have few values that I have added in application.yaml but I am not able to retrieve them in my test file eg. ownerMasterList value is coming as null but it is present in application.yaml
#ExtendWith(MockitoExtension.class)
public class OwnerServiceTest {
#Value("${owner.master-list}")
private ownerMasterList;
#Test
void findAllOwners(){
---test detail
}
}
Using ConfigFileApplicationContextInitializer alone does not provide support for #Value("${…​}") injection. Its only job is to ensure that application.properties files are loaded into Spring’s Environment. For #Value support, you need to either additionally configure a PropertySourcesPlaceholderConfigurer or use #SpringBootTest, which auto-configures one for you.
From doc
#RunWith(SpringRunner.class)
#ContextConfiguration(initializers = ConfigFileApplicationContextInitializer.class)
public class OwnerServiceTest {...}
Please try providing property source
By reading your code, it seems that you are using JUnit5 (#ExtendWith(MockitoExtension.class)).
The correct extension to use to trigger the launch of a Spring application context is (#ExtendWith(SpringExtension.class)). If you are using Spring Boot, you can use the #SpringBootTest annotation which is itself annotated with #ExtendWith(SpringExtension.class).
One point to note though: you should not wire an application context when unit testing your code. You should rather test your class in isolation and inject the needed property (here ownerMasterList) through the constructor (see #BeforeEach JUnit 5 annotation):
class OwnerServiceTest {
private OwnerService ownerService;
#BeforeEach
void setUp() {
this.ownerService = new OwnerService(new ArrayList<>(...));
}
}

How Do I Manually Wire A Spring Boot Integration Test?

Normally, I would test the web layer in a Spring project like this:
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SpringBootDemoApplicationTests extends AbstractTestNGSpringContextTests {
#LocalServerPort
int randomServerPort;
#Autowired
private TestRestTemplate restTemplate;
However, I currently have a difficult back end that requires a specific #TestConfiguration class to manually instantiate the test dependencies using beans.
This ultimately means that I can't use the #SpringBootTest annotation as it will try to create conflicting beans and fail to instantiate others.
If I am not using the #SpringBootTest annotation, I can manually create the TestRestTemplate instead of autowiring it, but what do I need to do to start the embedded local server on a random port?
I would still continue using #SpringBootTest, and combine that with using #Profile on your configuration classes.
That way you could have a configuration which is only used during tests, by using #ActiveProfiles on your #SpringBootTest classes. In the same way you can turn other config classes on or off depending on whether you want them to load or not.
For example on your test would have the following
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
#ActiveProfiles("unittest")
public class SpringBootDemoApplicationTests extends AbstractTestNGSpringContextTests {
...
}
Then create a configuration class which will instantiate your components the way you want them in test
#Profile("unittest")
#Configuration
public void TestConfiguration {
...
}
And you can use profiles to stop your other configuration class from loading during tests.
#Profile("!unittest")
#Configuration
public void ProdConfiguration {
...
}

How to exclude/disable a specific auto-configuration in Spring boot 1.4.0 for #DataJpaTest?

I am using the #DataJpaTest from Spring for my test which will then use H2 as in memory database as described here . I'm also using Flyway for production. However once the test starts FLyway kicks in and reads the SQL file. How can I exclude the FlywayAutoConfiguration and keep the rest as described here in spring documentation in order to let Hibernate create the tables in H2 for me?
#RunWith(SpringRunner.class)
#DataJpaTest
public class MyRepositoryTest {
#Autowired
private TestEntityManager entityManager;
#Autowired
private MyRepository triggerRepository;
}
Have you tried the #OverrideAutoConfiguration annotation?
It says it "can be used to override #EnableAutoConfiguration".
I'm assuming that from there you can somehow exclude FlywayAutoConfiguration
like so:
#EnableAutoConfiguration(exclude=FlywayAutoConfiguration.class)
Adding the dependency on an in-memory database to my build.gradle
e.g. testRuntime "com.h2database:h2:1.4.194"
And adding flyway.enabled=false to application.properties in src/test/resources worked for me.
I am converting an old JDBC app into a spring-data-jpa app and I'm working on the first tests now. I kept seeing a security module instantiation error from spring-boot as it tried to bootstrap the security setup, even though #DataJpaTest should theoretically be excluding it.
My problem with the security module probably stems from the pre-existing implementation which I inherited using PropertySourcesPlaceholderConfigurer (via my PropertySpringConfig import below)
Following the docs here:
http://docs.spring.io/spring-boot/docs/1.4.x/reference/htmlsingle/#test-auto-configuration
and your comments on #LiviaMorunianu's answer, I managed to work my way past every spring-boot exception and get JUnit to run with an auto-configured embedded DB.
My main/production spring-boot bootstrap class bootstraps everything including the stuff I want to exclude from my tests. So instead of using #DataJpaTest, I copied much of what it is doing, using #Import to bring in the centralized configurations that every test / live setup will use.
I also had issues because of the package structure I use, since initially I was running the test which was based in com.mycompany.repositories and it didn't find the entities in com.mycompany.entities.
Below are the relevant classes.
JUnit Test
#RunWith(SpringRunner.class)
#Transactional
#Import({TestConfiguration.class, LiveConfiguration.class})
public class ForecastRepositoryTests {
#Autowired
ForecastRepository repository;
Forecast forecast;
#Before
public void setUp() {
forecast = createDummyForecast(TEST_NAME, 12345L);
}
#Test
public void testFindSavedForecastById() {
forecast = repository.save(forecast);
assertThat(repository.findOne(forecast.getId()), is(forecast));
}
Live Configuration
#Configuration
#EnableJpaRepositories(basePackages = {"com.mycompany.repository"})
#EntityScan(basePackages = {"com.mycompany.entity"})
#Import({PropertySpringConfig.class})
public class LiveConfiguration {}
Test Configuration
#OverrideAutoConfiguration(enabled = false)
#ImportAutoConfiguration(value = {
CacheAutoConfiguration.class,
JpaRepositoriesAutoConfiguration.class,
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
TransactionAutoConfiguration.class,
TestDatabaseAutoConfiguration.class,
TestEntityManagerAutoConfiguration.class })
public class TestConfiguration {
// lots of bean definitions...
}
PropertySpringConfig
#Configuration
public class PropertySpringConfig {
#Bean
static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer()
throws IOException {
return new CorePropertySourcesPlaceholderConfigurer(
System.getProperties());
}
}
In my particular case, i needed to disable the FlywayDB on in-memory integration tests. These are using a set of spring annotations for auto-configuring a limited applicationContext.
#ImportAutoConfiguration(value = TestConfig.class, exclude = FlywayAutoConfiguration.class)
the exclude could effectively further limit the set of beans initiated for this test
I had the same problem with my DbUnit tests defined in Spock test classes. In my case I was able to disable the Flyway migration and managed to initialize the H2 test database tables like this:
#SpringBootTest(classes = MyApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = ["flyway.enabled=false", "spring.datasource.schema=db/migration/h2/V1__init.sql"])
I added this annotation to my Spock test specification class. Also, I was only able to make it work if I also added the context configuration annotation:
#ContextConfiguration(classes = MyApplication.class)
I resolved the same issue by excluding the autoconfiguration from my application definition, i.e.
#SpringBootApplication(exclude = {FlywayAutoConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
you can also sue the following annotation:
#RunWith(SpringRunner.class)
#DataJpaTest(excludeAutoConfiguration = {MySqlConfiguration.class, ...})
public class TheClassYouAreUnitTesting {
}
You can just disable it in your test yaml file:
flyway.enabled: false

Resources