Spring boot with testcontainers - how to prevent DB initialization on context reload - java

Context
I have a suite of Integration tests in a Spring boot application. The test context uses a MSSQL docker container for it's database using the testcontainers framework.
Some of my tests use Mockito with SpyBean which, apparently by design, will restart the Spring context since the spied beans cannot be shared between tests.
Since I am using a non-embedded database that lives for the duration of all my tests, the database is provisioned by executing my schema.sql and data.sql at the start by using:-
spring.datasource.initialization-mode=always
The problem is that when the Spring context is restarted, my database is re-initialized again which triggers errors such as unique constraint issues, table already exists etc.
My parent test class is as follows if it's of any help:-
#ActiveProfiles(Profiles.PROFILE_TEST)
#Testcontainers
#SpringJUnitWebConfig
#AutoConfigureMockMvc
#SpringBootTest(classes = Application.class)
#ContextConfiguration(initializers = {IntegrationTest.Initializer.class})
public abstract class IntegrationTest {
private static final MSSQLServerContainer<?> mssqlContainer;
static {
mssqlContainer = new MSSQLServerContainer<>()
.withInitScript("setup.sql"); //Creates users/permissions etc
mssqlContainer.start();
}
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(final ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of("spring.datasource.url=" + mssqlContainer.getJdbcUrl())
.applyTo(configurableApplicationContext.getEnvironment());
}
}
}
Each integration test extends this so that the context (for non-spied tests) is shared and setup occurs just once.
What I want
I would like to be able to execute the startup scripts just one time on startup and never again despite any number of context reloads. If the Spring test framework could remember that I already have a provisioned DB, that would be ideal.
I am wondering if there are any existing configurations or hooks that may help me
If something like the following existed, it'd be perfect.
spring.datasource.initialization-mode=always-once
But, as far as I can tell, it doesn't :(
Possible, but incomplete, solutions
Test container init script
new MSSQLServerContainer<>().withInitScript("setup.sql");
This works and ensures I can run a startup script the first time only since the container is started up just once. However withInitScript only takes a single argument rather than an array. As such, I would need to concatenate all my scripts into one file which means I'd have to maintain two sets of scripts.
If you only had one script, this would work fine.
Continue on error
spring.datasource.continue-on-error=true
This works in the sense that startup errors in the schema are ignored. But.. I want it to fail on startup if someone put some dodgy SQL in the scripts.
Spring event hooks
I couldn't get this to work. My idea was that I could listen for the ContextRefreshedEvent and then inject a new value for spring.datasource.initialization-mode=never.
It's a bit of a hack but I tried something like the following
#Component
public static class EventListener implements ApplicationListener<ApplicationEvent> {
#Autowired
private ConfigurableEnvironment environment;
#Override
public void onApplicationEvent(final ApplicationEvent event) {
log.info(event.getClass().getSimpleName());
if (event instanceof ContextRefreshedEvent) {
TestPropertyValues.of("spring.datasource.initialization-mode=never")
.applyTo(this.environment);
}
}
}
My guess is when the context restarts, it will also reload all my original property sources again which has mode=always. I would need an event right after the properties are loaded and right before the schema creation occurs.
So with that, does anyone have any suggestions?

So I ended up finding a workaround for this. Feels hacky but unless someone else is able to suggest a more appropriate and less obscure fix, then this is what I'll go with.
The solution uses a combination of #tsarenkotxt suggestion of AtomicBoolean and my #3 partial solution.
#ActiveProfiles(Profiles.PROFILE_TEST)
#Testcontainers
#SpringJUnitWebConfig
#AutoConfigureMockMvc
#SpringBootTest(classes = Application.class)
#ContextConfiguration(initializers = {IntegrationTest.Initializer.class})
public abstract class IntegrationTest {
private static final MSSQLServerContainer mssqlContainer;
//added this
private static final AtomicBoolean initDB = new AtomicBoolean(true);
static {
mssqlContainer = new MSSQLServerContainer()
.withInitScript("setup.sql"); //Creates users/permissions etc
mssqlContainer.start();
}
static class Initializer implements ApplicationContextInitializer {
#Override
public void initialize(final ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=" + mssqlContainer.getJdbcUrl(),
//added this
"spring.datasource.initialization-mode=" + (initDB.get() ? "always" : "never"))
.applyTo(configurableApplicationContext.getEnvironment());
//added this
initDB.set(false);
}
}
}
Basically I set spring.datasource.initialization-mode to be always on the very first startup, since the db hasn't been setup yet, and then reset it to never for every context initialization thereafter. As such, Spring won't attempt to execute the startup scripts after the first run.
Works great but I don't like having to hide this configuration here so still hoping someone else will come up with something better and more "by design"

Related

#BeforeAll JUnit/spring-boot-test alternative that runs when application context starts

I'm writing a #Repository/#Service integration test that leverages an embedded database. In my test class, I would like to preload my database with some data.
I'm currently using #BeforeEach to load in my sample data, however, this code is run upon each test in my class.
Is there any way that I can load in my test data after Spring application context has loaded, but before any test has been run?
My current approach:
#BeforeEach
public void before() {
repository.save(...); // -> prepopulates repository with sample data
}
#Test
public void testService() {
service.get(...); // -> gathers existing record
}
#Test
public void deleteById() {
service.delete(...); // -> deletes existing record
}
However... with this, I am required to flush out the records after every test. Otherwise any unique constraints can easily be violated.
Rather than using #BeforeEach which is required to run before every test... is it possible to load this in in a #BeforeAll kind of fashion that happens after the spring application context has been loaded?
Is there any way that I can load in my test data after Spring application context has loaded
Basically yes, I think you can do that:
The idea is to load the SQL data when the application context is started or in the process of being started.
For example, spring boot integration with Flyway works this way (the bean of Flyway is created and loaded). So, in theory, you could merely use Flyway with test migrations that will contain all the relevant SQL scripts of test data generation.
How can you do this technically?
Here is one way:
Create a special bean (just like the way it works with Flyway) that would depend on your repository and in post construct save the data:
#Component
public class SqlGenerationBean {
#Autowired
private MyRepository repo;
#PostConstruct
public void init() {
repo.save();
}
}
Another way of doing is to create a listener that will be called upon the application context started and again will call the same repo.save().
In both cases the bean/listener code should not be accessible from production (it's only for tests): so put it somewhere under src/test/java for example
Now once the application context is started you can use a neat trick:
Mark your tests with #Transactional annotation. Spring will wrap the code in an artificial transaction that will be rolled back automatically (even if the test succeeds) so that all the data that you'll modify during the test will be rolled back and basically before each test, you'll have the same state (that is identical to the state of the database when/after the application context starts). Of course, if you use DDL in the test, some databases can't make it a part of transaction but it depends on the database really.
Another interesting point here is that the application context can be cached even between the test cases (created only once), so keep this in mind.
In this case I would just create a constructor for the test class. It will be triggered before everything.
#BeforeEach runs before each tests but after all initialisations .
you can also just use Mockito and mock the result without need to clean and overcomplicate
Just add following snippet to your code. This is just like you can do to detect that Spring application is really started.
#Configuration
public class AppConfig implements ApplicationListener<ApplicationReadyEvent> {
/**
* This is to indicate in the logs when the application has actually started and everything is loaded.
*/
#Override
public void onApplicationEvent(ApplicationReadyEvent event) {
ApplicationContext context = event.getApplicationContext();
Environment env = context.getEnvironment();
// do what you want on application start
}
}
P.S. For database manipulation in test #Sql is the best candidate as was mentioned in comment.

Spring issue with #PostConstruct and #PreDestroy

I have a Spring application that I am trying to test with EmbededRedis. So I created a component like below to Initialize and kill redis after test.
#Component
public class EmbededRedis {
#Value("${spring.redis.port}")
private int redisPort;
private RedisServer redisServer;
#PostConstruct
public void startRedis() throws IOException {
redisServer = new RedisServer(redisPort);
redisServer.start();
}
#PreDestroy
public void stopRedis() {
redisServer.stop();
}
}
But now I am facing a weird issue. Because spring caches the context, PreDestroy doesnt get called everytime after my test is executed, but for some reason, #PostConstruct gets called, and EmbededRedis tries to start the running redis server again and again, which is creatimg issues in the execution.
Is there a way to handle this situation by any mean?
Update
This is how I am primarily defining my tests.
#SpringBootTest(classes = {SpringApplication.class})
#ActiveProfiles("test")
public class RedisApplicationTest {
Ditch the class and write an #Configuration class which exposed RedisServer as a bean.
#Configuration
public void EmbeddedRedisConfiguration {
#Bean(initMethod="start", destroyMethod="stop")
public RedisServer embeddedRedisServer(#Value("${spring.redis.port}") int port) {
return new RedisServer(port);
}
}
So I debuged the ContextInitialization as suggested by #M. Deinum.
For me, the porblem was, Our application was mocking different classes in order to mix mocking with Spring context.
Now, when you use mocks, MockitoContextInitializer also becomes part of your cache key, which results in cache miss. Reason is, The classes under mock are obviously different for different test classes.
Looking at the situation, I preferred to go ahead with #DirtiesContext to invalidate the contest after the test is done, so that I can reinitialize the context later on for different test.
Note #DirtiesContext is in a way recommended to be avoided as it slows down your tests.

Skipping integration tests in spring boot on VPN DNS availability

I have a moderately heavy springboot service, it takes 10-15 seconds to boot on a happy flow, and (1-2) minutes to fail on a retry/failover flow. This is ok for my business flows, and is how I expect a healthy service to behave.
I have integration tests (that run some end-to-end flows in my service), that can only test the actual integration status while the test machine (or dev machine) is connected to a specific VPN.
I want to auto skip integration tests if I'm not connected to VPN.
consider the following code
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {Server.class}, // auto scans a bunch of components
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // slow loading context
public class IntegrationTest {
#BeforeClass
public static void beforeClass() {
Assume.assumeTrue(DnsTool.vpnConnected()); // fast failing test
}
#Test
public void testIntegration() {
// some test logic
}
}
When the assumptions pass, my tests run, and all is good.
When the assumptions fail, my tests get skipped, but only after trying to load my expensive context.
How can I avoid the long running time for my test suite?
Things I tried:
Subclassing SpringJUnit4ClassRunner, and overriding isTestMethodIgnored.
Adding a TestExecutionListener, and throwing the assumption exception in beforeTestClass
These made no impression on Spring, and the context got loaded any way.
Things I didn't try:
Lazy init comes with 2.2.X next stable release of spring I think.
Lazy init potentially makes my problem go away, but I feel like there should be some easy spring-test/junit fix that I'm missing.
Thanks in advance for the help.
To me, this sounds like something that you shouldn't do in tests at all.
Tests (at least IMHO), are supposed to check the business cases and assume that the environment is set up and ready.
Maybe it worth to delegate this functionality to build tool and CI.
Example:
Define a profile in maven (or whatever build tool you use) that will run integration tests that require VPN. Define profile that will run all the rest of integration tests as well.
Activate the profile if some system property is available.
In CI tool (like Jenkins) as a part of CI even before you run maven, run the script that will check the VPN connection. Based on the results set the system properties and run maven with these properties. The required profiles will be loaded and all the tests / only tests that do not require VPN will be run.
Update
If you need to make it work from Spring (and it looks like you prefer this way),
Spring has a special annotation called #IfProfileValue
By default, it matches against system properties and if the value doesn't match the test gets ignored.
It looks something like this (and note that you can put this annotation on class as well, then it will work for all test methods in the class):
#RunWith(SpringRunner.class)
#SpringBootTest
public class MyTestClass {
#IfProfileValue(name = "os.name", values = {"Linux"})
#Test
public void testMe() {
// will run only in linux, otherwise
// won't even try to load an
// application context
....
}
}
This covers the case when you resolve the VPN connectivity externally and run the tests with a property. However, if you want to implement the VPN connectivity check in java, this annotation along not enough because it can work only with Java system properties, so in order to work with custom logic you need to implement org.springframework.test.annotation.ProfileValueSource:
public class VPNConnectivityProfileValueSource implements ProfileValueSource {
private String vpnEnabled = "true";
public VPNConnectivityProfileValueSource () {
// no spring context is available here
ClassPathResource resource = new ClassPathResource("vpn-config.properties");
if (resource.exists()) {
// read the VPN address,
//
//this.testProps = PropertiesLoaderUtils.loadProperties(resource);
// invoke your utility, check the connectivity, etc.
this.vpnEnabled = ...
}
}
#Override
public String get(String key) {
// this is important method,
if(key.equals("vpn.enabled") {
return this.vpnEnabled;
}
else return System.getProperty(key);
}
}
The last thing is to make the test aware of the ProfileValueSource:
For this there is another special annotation that you put on the test:
#ProfileValueSourceConfiguration(VPNConnectivityProfileValueSource.class)
All in all it the test can look like this:
#RunWith(SpringRunner.class)
#SpringBootTest
#ProfileValueSourceConfiguration(VPNConnectivityProfileValueSource.class)
#IfProfileValue(name = "vpn.enabled", value = "true")
public class MyTestClass {
#Test
public void testMe() {
....
}
}
All the classes/annotations I've mentioned reside in package org.springframework.test.annotation

Appropriate usage of TestPropertyValues in Spring Boot Tests

I came across TestPropertyValues, which is briefly mentioned in the Spring Boot docs here: https://github.com/spring-projects/spring-boot/blob/2.1.x/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc#testpropertyvalues
It's also mentioned in the Migration Guide here: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#environmenttestutils
Both examples show an environment variable to apply the properties to, but there's no other documentation that I could find.
In my tests the property setting comes too late to affect the property injection (via #Value) for a Spring Bean. In other words, I have a constructor like this:
public PhoneNumberAuthorizer(#Value("${KNOWN_PHONE_NUMBER}") String knownRawPhoneNumber) {
this.knownRawPhoneNumber = knownRawPhoneNumber;
}
Since the above constructor is called before the test code has a chance to run, there's no way change the property via TestPropertyValues in the test before it's used in the constructor.
I understand that I can use the properties parameter for #SpringBootTest, which updates the environment before beans get created, so what's the appropriate usage of TestPropertyValues?
TestPropertyValues isn't really designed with #SpringBootTest in mind. It's much more useful when you are writing tests that manually create an ApplicationContext. If you really want to use it with #SpringBootTest, it should be possible to via an ApplicationContextInitializer. Something like this:
#RunWith(SpringRunner.class)
#SpringBootTest
#ContextConfiguration(initializers = PropertyTest.MyPropertyInitializer.class)
public class PropertyTest {
#Autowired
private ApplicationContext context;
#Test
public void test() {
assertThat(this.context.getEnvironment().getProperty("foo")).isEqualTo("bar");
}
static class MyPropertyInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertyValues.of("foo=bar").applyTo(applicationContext);
}
}
}
Spring Boot's own test make use of TestPropertyValues quite a bit. For example, applyToSystemProperties is very useful when you need to set system properties and you don't want them to be accidentally left after the test finishes (See EnvironmentEndpointTests for an example of that). If you search the codebase you'll find quite a few other examples of the kinds of ways it usually gets used.

Junit Spring avoid to load twice application context datasource

I have this configuration classes:
#ComponentScan(
basePackages = {
"mypackage.controller",
"mypackage.service",
"mypackage.repository"
}
)
#TestPropertySource(locations="classpath:configuration.properties")
#Import({
H2Configuration.class
})
public class TestConfiguration {
}
#Configuration
public class H2Configuration {
#Bean
public DataSource dataSource() throws SQLException {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
EmbeddedDatabase db = builder
.setType(EmbeddedDatabaseType.H2)
.addScript("h2/create.sql")
.addScript("h2/insert.sql")
.build();
db.getConnection().setAutoCommit(false);
return db;
}
}
And I have this two class tests:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(loader=AnnotationConfigContextLoader.class, classes = { TestConfiguration.class })
public class FirstRepositoryTest {
#Autowired
MyFirstRepositoryImpl repository;
#Before
public void initTest() {
}
#Test(expected = NullPointerException.class)
public void testNullRecords() {
repository.foo(null, null);
}
}
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(loader=AnnotationConfigContextLoader.class, classes = { TestConfiguration.class })
public class SecondRepositoryTest {
#Autowired
MySecondRepositoryImpl repository;
#Before
public void initTest() {
}
#Test(expected = NullPointerException.class)
public void testSomethingNullRecords() {
repository.something(null, null);
}
}
If I run junit test once for each class, all goes well.
In clean install phase tests fails because the application context is initialized twice.
For example it try to create the h2 tables twice and do the insert.sql script twice.
What I have to do for initialize the h2 database and so application context only once?
Thanks
I think you could start looking at the Spring documentation about Integration Testing.
It can also be a good practice to use transactional tests for integration tests (#Transactional), which rollback at the end of each test : see Transaction Management.
To avoid the cost of recreating the ApplicationContext for each test class, the cache may be used as explained here : Context Caching.
For integration testing with Embedded Database, you can also find documentation : Testing Data Access Logic with an Embedded Database.
A note from the previous link, matching your use case :
However, if you wish to create an embedded database that is shared
within a test suite, consider using the Spring TestContext Framework
and configuring the embedded database as a bean in the Spring
ApplicationContext as described in Creating an Embedded Database by
Using Spring XML and Creating an Embedded Database Programmatically.
I hope you will find some useful references.
Another good tip I found from Spring Boot documentation from Embedded Database Support :
They say :
If you are using this feature in your tests, you may notice that the
same database is reused by your whole test suite regardless of the
number of application contexts that you use. If you want to make sure
that each context has a separate embedded database, you should set
spring.datasource.generate-unique-name to true.
So to make each EmbeddedDatabase unique, you may try to create them with :
EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
...
.build();
In unit testing you must garantee that every test is repeatible hance context independent. Due to this is not good idea to load the context only once. Is better to reset after the execution. For this you can use #DirtiesContext(classMode = ClassMode.AFTER_CLASS) in your test classes
So you will force your context to restart when the next junit class is launched
So the reason that this is failing is that the database (H2) is resident in memory when you run the tests as part of clean/install. The create/insert scripts have already executed after the first test is run. Any subsequent test execution after this point will result in a re-execution of the same script(s) and the error will occur.
Update your create script with a DROP TABLE IF EXISTS <table name>;. This will ensure that the table is dropped then recreated.
NOTE: I'm not sure why you've specified AnnotationConfigContextLoader explicitly. I think, without that, the runner SpringJUnit4ClassRunner will cache contexts that have not been changed. I don't know specifically if that is the case here though.

Categories

Resources