Dynamically add property sources to SpringBootTest - java

Similar to Springboot unit test set #Configuration Properties dynamically but the context is different.
In my case I have a TestContainer running a custom MySQL database that is prepopulated with a lot of data (not using the SQL batch loading approach because the data is an anonymized copy of production and doing it through SQLs makes the boot up time of the container 20 minutes vs 2 minutes).
So far my test looks like this
#RunWith(SpringRunner.class)
#SpringBootTest(
classes = {
Bootstrap.class
}
)
public class ITFakeDB {
#ClassRule
public static final GenericContainer DB = new GenericContainer("devdb")
.withExposedPorts(3306);
#Autowired
private DataSource dataSource;
#Autowired
private Users users;
#Test
public void testDatabaseIsUp() {
assertTrue(DB.getMappedPort(3306) != 0);
}
#Test
public void testUser() {
Optional<User> user = users.findByLoginName("mimi");
assertTrue(users.isPresent());
}
}
What I want to do is somehow set the spring.datasource.url (or in my case datasources.schema1.url because I did the routing datasource) to the one used by DB

You can manually override the property from within your Spring-boot test by using ContextConfiguration and ApplicationContextInitializer.
Override the property - define a static inner class:
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
String url = "jdbc:mysql://" + DB.getContainerIpAddress() + ":" + DB.getMappedPort(3306) + "/my_db";
TestPropertyValues
.of("datasources.schema1.url=" + url)
.applyTo(configurableApplicationContext.getEnvironment());
}
}
Note: I have assumed that the url is derived from the ip address, port and db name. You may change that part as needed but the core idea remains.
ApplicationContextInitializer can be used for programmatically initializing a Spring context before context refresh. Now, wire up the context initializer class by annotating at test class level with ContextConfiguration:
#ContextConfiguration(initializers = Initializer.class)
Docs:
ApplicationContextInitializer
ContextConfiguration

While the previous answer should work, Spring Framework 5.2.5 (that is included into Spring Boot 2.2.6) has introduced a new #DynamicPropertySource annotation exactly for that case:
#DynamicPropertySource
static void initializeDatasource(DynamicPropertyRegistry registry) {
String ip = DB.getContainerIpAddress();
Integer port = DB.getMappedPort(3306);
String url = String.format("jdbc:mysql://%s:%d/my_db", ip, port);
registry.add("datasources.schema1.url", url);
}
See for details:
Blog: #DynamicPropertySource in Spring Framework 5.2.5 and Spring Boot 2.2.6
Documentation: Context Configuration with Dynamic Property Sources

Related

How to configure the database image for Springboot integration testing

There is a situation that I want to use a database image to integration test my service layer. Here is the code I developed to setup my Postgres container image:
#SpringBootTest(classes = EventhandlerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureMockMvc
public abstract class BaseIT {
public static final PostgreSQLContainer<?> postgresDB = new PostgreSQLContainer<>
("postgres:12-alpine")
.withDatabaseName("test-db")
.withUsername("postgres")
.withPassword("password");
static {
postgresDB.start();
}
#DynamicPropertySource
public static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgresDB::getJdbcUrl);
registry.add("spring.datasource.username", postgresDB::getUsername);
registry.add("spring.datasource.password", postgresDB::getPassword);
}
}
Now I want to use the test database to call my service layer methods and inspect the results, here is my integration test example:
public class SampleServiceTesting extends BaseIT {
#Autowired
private SampleService sampleService;
#Test
#Transactional
void testIntegrationFlow() {
SampleService.exampleMethod();
}
}
But when I run the test, it returns the following error:
org.hibernate.HibernateException: Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set
How I can fix this issue on runing my test please?
You asked
Experiencing HibernateException in testcontainers with using PostgreSQLContainer
The problem lies on not having active connection into the database through a connection pool which can also cause from miss-configuration and incorrect injection of PostgreSQLContainer, cause Hibernate can determine the correct dialect to use automatically, and for doing this it needs a live connection to the database.
One practice could be using of ApplicationContextInitializer interface instead of #DynamicPropertySource for programmatic initialization of the application context. For example, registering datasources or activating profiles against the context's environment.
One practice will look like below.
static class PropertyInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=" + postgresDB.getJdbcUrl(),
"spring.datasource.username=" + postgresDB.getUsername(),
"spring.datasource.password=" + postgresDB.getPassword()
).applyTo(configurableApplicationContext.getEnvironment());
}
}

Add environment property to #SpringBootTest before application.properties evaluation?

Is it possible to add a "dynamic" property from a class to application.properties before they are evaluated?
I want to achieve the following: find a free port on the system, inject it as property mockPort into the spring lifecycle, and reuse this port to override a property from application.properties using #TestPropertySource, as follows:
#SpringBootTest
#TestPropertySource(properties = "web.client.url=localhost:${mockPort}/path")
public class MyWebTest {
//TODO how to write the port into ${mockPort} property?
private static final PORT = SocketUtils.findAvailableTcpPort();
#BeforeEach
public void init() {
MockWebServer mock = new MockWebServer();
mock.start(PORT);
}
#Test
public void test() {
service.runWebRequest();
}
}
The service under test can be any client that makes use of #Value("${web.client.url}). And as the free port is found dynamically during runtime, I have to somehow inject it into that property.
How can I achieve this?
You can use Spring Framework's #DynamicPropertySource for this purpose. It's described in this blog post.
In the case of MyWebTest, the dynamic property source would look something like this:
#DynamicPropertySource
static void mockPortProperty(DynamicPropertyRegistry registry) {
registry.add("mockPort", () -> PORT);
}
Maybe you prefer to start a single MockWebServer for all test classes, instead of starting a new server for each test method. When this class is initialized, the static block starts a server and sets a system property to the server URL. System properties override properties from the application.properties file.
#TestConfiguration
public class MockWebServerConfiguration {
private static final MockWebServer mockWebServer = new MockWebServer();
static {
int port = SocketUtils.findAvailableTcpPort();
mockWebServer.start(port);
System.setProperty("web.client.url", "localhost:" + port + "/path");
}
}
To include this configuration class in your integration test, annotate your test class with:
#Import(MockWebServerConfiguration.class)

How to pass arguments to Spring Repository when using it with a Spring Application or while testing

I've created a Spring repository, that allows the user of this repository to add and remove from a MySQL table. Now to be able to use this repository, the application/tester would need to pass in the datasource variables (viz URL of the database, usermame, and password).
I tried having arguments to my constructor for the repository, but it seems that that is not allowed.
From what I've read online, one way is to use #Value annotation, and use that to pass it in. However, does this also work if the applciation.properties is not in the same file as the repository? By that I mean, like, the person making the application would put the application.properties file in the directory of the application right? And not in the directory of the repository? How do I do it in that case?
Any help is appreciated, thank you
Here is the code for my repository right now:
#Repository
public class SigningKeyDao implements IDao<SigningKeyModel> {
private DataSource dataSource;
private JdbcTemplate jdbcTemplate;
SigningKeyRowMapper wrapper = new SigningKeyRowMapper();
private String tableName = "signingKey";
public SigningKeyDao(String url, String username, String password) {
super();
this.dataSource =
DataSourceBuilder.create().url(url).username(username).password(password).build();
}
#PostConstruct
private void postConstruct() {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
}
And this is how I am using this repository in my test class
#EnableJpaRepositories("com.supertokens.repository")
#SpringBootTest(classes = SigningKeyDao.class)
#EntityScan("com.supertokens.model")
public class SigningKeyTableTest {
SigningKeyDao dao;
public SigningKeyTableTest() {
this.dao =
new SigningKeyDao("jdbc:mysql://localhost:3306/st_maven", "root", <password>);
}
}
To set an application property to a certain value in a particular test, have a look at the #TestPropertySource annotation which allows you to change the value of one or more properties from the application.properties file for the scope of the annotated test-class.
Example:
#EnableJpaRepositories("com.supertokens.repository")
#SpringBootTest(classes = SigningKeyDao.class)
#EntityScan("com.supertokens.model")
#TestPropertySource(properties = {
"spring.datasource.url=custom_url_just_for_this_test",
"spring.datasource.username=user",
"spring.datasource.password=secret"
})
public class SigningKeyTableTest {

Why isn't my spring boot (mongo) bean being created / used?

I'm trying to use SpringBoot to talk to a Mongo database.
It is working using spring-boot-starter-data-mongodb and auto configuring a default bean which does allow my MongoRepository classes to talk to the DB ok.
However, I want to override the defaults. I could use application.properties but I need to be able to pass the connection parameters as options on the command line as the application starts up.
I've tried changing the port to break it, I've added debug to the Mongo config and it seems whatever I do the default spring config is being used regardless. It's as if the #Configuration annotation is ignored.
I've tried various flavours of configuring the main application class (specifying conf location, adding #Configuration to main class, with and without #SpringBootApplication ...), but here is where I am at the moment....
package somepackage
#EnableAutoConfiguration
#ComponentScan
public class MyApplication {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(MyApplication.class, args);
....
}
package somepackage.conf; // should be picked up by ComponentScan, no?
#Configuration
public class MongoConf {
#Bean
public MongoClientFactoryBean mongo() throws Exception {
MongoClientFactoryBean mongo = new MongoClientFactoryBean();
/*
setting to silly values to try to prove it is trying to create connections using this bean - expected to see errors because can't create connection... */
mongo.setHost("flibble");
mongo.setPort(345);
return mongo;
}
}
You should actually use built in Spring Boot MongoDb Starter features and related auto configuration through application properties. Custom host, port, passwords etc. can and should be set via dedicated Spring Boot MongoDB Properties:
spring.data.mongodb.authentication-database= # Authentication database name.
spring.data.mongodb.database=test # Database name.
spring.data.mongodb.field-naming-strategy= # Fully qualified name of the FieldNamingStrategy to use.
spring.data.mongodb.grid-fs-database= # GridFS database name.
spring.data.mongodb.host=localhost # Mongo server host.
spring.data.mongodb.password= # Login password of the mongo server.
spring.data.mongodb.port=27017 # Mongo server port.
spring.data.mongodb.repositories.enabled=true # Enable Mongo repositories.
spring.data.mongodb.uri=mongodb://localhost/test # Mongo database URI. When set, host and port are ignored.
spring.data.mongodb.username= # Login user of the mongo server.
And link to the full list of supported properties is here.
In addition to RafalG's suggestion about MongoProperties, I combined that with the ApplicationArguments class and now I'm getting somewhere....
#Bean
#Primary
public MongoProperties mongoProperties(ApplicationArguments args) {
MongoProperties props = new MongoProperties();
String[] mongoHostAndPort = args.getSourceArgs()[3].split(":");
props.setHost(mongoHostAndPort[0]);
props.setPort(Integer.parseInt(mongoHostAndPort[1]));
return props;
}
#Bean
public MongoClientFactoryBean mongo() {
return new MongoClientFactoryBean();
}
Of course there's lots of error handling to add (nulls, non-ints etc) but hopefully if may help someone else.
#Configuration
#EnableAutoConfiguration(exclude = { EmbeddedMongoAutoConfiguration.class })
#Profile("!testing")
public class TestMongoConfig extends AbstractMongoConfiguration {
private static final MongodStarter starter = MongodStarter.getDefaultInstance();
private MongodExecutable _mongodExe;
private MongodProcess _mongod;
private MongoClient _mongo;
#Value("${spring.data.mongodb.host}")
private String host;
#Value("${spring.data.mongodb.port}")
private Integer port;
#Override
protected String getDatabaseName() {
return "test";
}
#Bean
public Mongo mongo() throws Exception {
_mongodExe = starter.prepare(new MongodConfigBuilder()
.version(Version.Main.PRODUCTION)
.net(new Net(port, Network.localhostIsIPv6()))
.build());
_mongod = _mongodExe.start();
return new MongoClient(host, port);
}
#Override
public String getMappingBasePackage() {
return "com.test.domain";
}

Changing Spring Boot Properties Programmatically

I'm trying to write tests for an application that uses #RefreshScope. I would like to add a test that actually changes out properties and asserts that the application responds correctly. I have figured out how to trigger the refresh (autowiring in RefreshScope and calling refresh(...)), but I haven't figured out a way to modify the properties. If possible, I'd like to write directly to the properties source (rather than having to work with files), but I'm not sure where to look.
Update
Here's an example of what I'm looking for:
public class SomeClassWithAProperty {
#Value{"my.property"}
private String myProperty;
public String getMyProperty() { ... }
}
public class SomeOtherBean {
public SomeOtherBean(SomeClassWithAProperty classWithProp) { ... }
public String getGreeting() {
return "Hello " + classWithProp.getMyProperty() + "!";
}
}
#Configuration
public class ConfigClass {
#Bean
#RefreshScope
SomeClassWithAProperty someClassWithAProperty() { ...}
#Bean
SomeOtherBean someOtherBean() {
return new SomeOtherBean(someClassWithAProperty());
}
}
public class MyAppIT {
private static final DEFAULT_MY_PROP_VALUE = "World";
#Autowired
public SomeOtherBean otherBean;
#Autowired
public RefreshScope refreshScope;
#Test
public void testRefresh() {
assertEquals("Hello World!", otherBean.getGreeting());
[DO SOMETHING HERE TO CHANGE my.property TO "Mars"]
refreshScope.refreshAll();
assertEquals("Hello Mars!", otherBean.getGreeting());
}
}
You could do this (I assume you mistakenly omitted the JUnit annotations at the top of your sample, so I'll add them back for you):
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
public class MyAppIT {
#Autowired
public ConfigurableEnvironment environment;
#Autowired
public SomeOtherBean otherBean;
#Autowired
public RefreshScope refreshScope;
#Test
public void testRefresh() {
assertEquals("Hello World!", otherBean.getGreeting());
EnvironmentTestUtils.addEnvironment(environment, "my.property=Mars");
refreshScope.refreshAll();
assertEquals("Hello Mars!", otherBean.getGreeting());
}
}
But you aren't really testing your code, only the refresh scope features of Spring Cloud (which are already tested extensively for this kind of behaviour).
I'm pretty sure you could have got this from the existing tests for refresh scope as well.
Properties used in the application must be variables annotated with #Value. These variables must belong to a class that is managed by Spring, like in a class with the #Component annotation.
If you want to change the value of the properties file, you can set up different profiles and have various .properties files for each profile.
We should note that these files are meant to be static and loaded once, so changing them programmatically is sort of out of the scope of ther intended use. However, you could set up a simple REST endpoint in a spring boot app that modifies the file on the host's file system (most likely in the jar file you are deploying) and then calls Refresh on the original spring boot app.

Categories