How to use Kotlin beans dsl initializer in SpringBootTest - spring-boot

I have a simple application with several beans declared with kotlin beans dsl:
#SpringBootApplication
class App
val beans = beans {
bean<A>()
}
fun main(args: Array<String>) {
runApplication<MatchmakerApp>(*args) {
addInitializers(beans)
}
}
#RestController
class AppController(val a: A) {
// some code
}
class A
and I have an integration test:
#RunWith(SpringRunner::class)
#SpringBootTest
class AppControllerTest {
#Test
fun dummyTest() {
assert(true)
}
}
Launching this test I'm getting
UnsatisfiedDependencyException: Error creating bean with name appController
Caused by: NoSuchBeanDefinitionException: No qualifying bean of type 'A' available:`
It seems beans initializer was not invoked during SpringBootTest context creation.
What do we need to add kotlin bean dsl initializer in SpringBootTest?
The general way with #ContextConfiguration(initializers = ...) does not work here, because it looks for classes.

add FuBeansInitializer in the same package with App class in the test directory:
class FuBeansInitializer : ApplicationContextInitializer<GenericApplicationContext> {
override fun initialize(context: GenericApplicationContext) = beans.initialize(context)
}
add context.initializer.classes into test application.properties:
context.initializer.classes=path.to.FuBeansInitializer
As a result, there will be nothing modified in the source files. And tests will work fine.

You can even have multiple ApplicationContextInitializer and provide a comma separated list of them in your properties(order matters). This can be useful if you used Initializer in your main code and would want to override some of your beans using, again, bean definition dsl.

Related

Is there a way to overide automatically injected beans in Spring boot when writing tests?

I have a class annotated with a spring bean #Repository("clientDatasource") called ClientServiceDatasource which implements an interface called Datasource. I also have a mock implementation of this interface also annotated with a spring bean #Repository("mockDatasource") called MockClientServiceDatasource. I also have a class annotated with the spring bean #Service called ClientService and in in its constructor, I pass in a datasource. I do it like so:
#Service
class ClientService (#Qualifier("clientDatasource") private val dataSource: Datasource){}
As you can see that the service will default to the clientDatasource, because of the #Qualifier when the application is running.
However when I run my tests I annotate my test class with #SpringTest . In my understanding this means that it boots up the entire application as if it were normal. So I want to somehow overide that #Qualifier bean thats being used in the client service in my test so that the Client Service would then use the mockedDatasource class.
I'm fairly new to kotlin and spring. So I looked around and found ways to write a testConfig class to configure beans like so :
#TestConfiguration
class TestConfig {
#Bean
#Qualifier("clientDatasource")
fun mockDatasource(): Datasource {
return MockClientServiceDatasource()
}
}
and then using it in the test like so:
#SpringTest
#Import(TestConfig::class)
class ClientServiceTest {
...
}
I also asked chatGPT and it gave me this:
#SpringBootTest
class ClientServiceTest {
#Autowired
lateinit var context: ApplicationContext
#Test
fun testWithMockedDatasource() {
// Override the clientDatasource bean definition with the mockDatasource bean
val mockDatasource = context.getBean("mockDatasource", Datasource::class.java)
val mockClientDatasourceDefinition = BeanDefinitionBuilder.genericBeanDefinition(MockClientServiceDatasource::class.java)
.addConstructorArgValue(mockDatasource)
.beanDefinition
context.registerBeanDefinition("clientDatasource", mockClientDatasourceDefinition)
// Now the ClientService should use the mockDatasource when it's constructed
val clientService = context.getBean(ClientService::class.java)
// ... do assertions and other test logic here ...
}
}
But some of the methods don't work, I guess chatGPT knowledge is outdated.
I also looked through spring docs, but couldn't find anything useful.
Okay, So I took a look at the code previously with the TestConfig class. And I realised by adding the:
#Primary
annotation to the method inside my TestConfig class, it basically forces that to be the primary repository bean. Like so:
#TestConfiguration
class TestConfiguration {
#Bean
#Primary
#Qualifier("clientDatasource")
fun mockDatasource(): Datasource {
return MockClientDataSource()
}
}
and in the test I only imported the test and it just worked. I didn't have to autowire anything
This is my test class:
#SpringBootTest
#AutoConfigureMockMvc
#Import(TestConfiguration::class)
internal class ServiceControllerTest{
#Suppress("SpringJavaInjectionPointsAutowiringInspection")
#Autowired
lateinit var mockMvc: MockMvc
#Test
fun `should return all clients` () {
// when/then
mockMvc.get("/clients")
.andDo { print() }
.andExpect {
status { isOk() }
content { contentType(MediaType.APPLICATION_JSON) }
jsonPath("$[0].first_name") {value("John")}
}
}
}

Functional bean definition Kotlin DSL - add initializer to single test class?

I have a Spring Boot test that is using Redis and I would like to load only Redis related
beans for that test.
I have a function that defines beans:
fun BeanDefinitionDsl.redisBeans() {
bean {
// ...
}
}
And I would like to have only those beans added to the single test class.
Is there a way to do so without adding bean initializer to application properties in the test resource folder?
It turns out that I can have normal Junit test (not a #SpringBootTest)
and get context built with initializers like:
SpringApplicationBuilder()
.bannerMode(Mode.OFF)
.initializers(MyCustomInitializer())
.sources(DummyConfig::class.java)
.properties(
mapOf(
"spring.autoconfigure.exclude" to "..."
)
)
.web(WebApplicationType.NONE)
.build()
.run()
.getBean(MyBeanThatIWantToTest::class.java)
// ...
#EnableAutoConfiguration
#Configuration
class DummyConfig

Error creating bean with name 'flywayInitializer' with flyway-test-extension

So I have two integration test classes. I am using the flyway-test-extension for resetting the db. When I run the test classes individually from IntelliJ, both pass. However, when I run them with mvn clean install or in IntelliJ all test together, TestClass2 fails with an exception.
Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Found non-empty schema(s) "public" without schema history table! Use baseline() or set baselineOnMigrate to true to initialize the schema history table.
I tried setting spring.flyway.baseline-on-migrate=true in the core_test.proberties file, but without success.
Difference between TestClass1 and TestClass2 is that #FlywayTest is once on class level and once on method level. Also in TestClass1 I use #MockBean for some of my services. Might this cause the error?
These are the tests I have:
#SpringBootTest(classes = { CoreTestConfig.class })
#TestPropertySource("classpath:core_test.properties")
#ExtendWith(SpringExtension.class)
#ExtendWith({FlywayTestExtension.class})
#FlywayTest // on class level
class TestClass1 {
// contains injected mocks #MockBean
}
#SpringBootTest(classes = { CoreTestConfig.class })
#TestPropertySource("classpath:core_test.properties")
#ExtendWith(SpringExtension.class)
#ExtendWith({FlywayTestExtension.class})
class TestClass2 {
#Test
#FlywayTest // on method level
void someTestMethod() {
// ...
}
}
and the context configuration for the test as follows:
#Configuration
#EnableAutoConfiguration
#EnableConfigurationProperties({
AppProperties.class
})
#EnableTransactionManagement
#EnableAsync
#EnableScheduling
#Import({ActivitiConfiguration.class})
#ComponentScan(basePackages = { "com.company.product.core" },
excludeFilters = #ComponentScan.Filter(value = Configuration.class)
)
#EntityScan(basePackageClasses = BaseDO.class)
#EnableJpaRepositories(basePackages = "com.company.product.core.repository")
public class CoreTestConfig {
// contains some specific test beans
}
I use org.flywaydb.flyway-test-extensions version 6.4.0.
UPDATE: When I remove #MockBean inside TestClass1, there are no more errors when running all tests together. But how am I supposed to use #MockBean with the flyway test extension?
As shown in this tutorial, mocking the beans inside of the CoreTestConfig class resolved the issue. I am not sure why but it works. And then simply #Autowire for the mocked services inside the tests.

How to get #Configuration files in Spring to run

I'm writing a micro service using dependency injection, and I have a Java class annotated with #Configuration which creates all my beans. However, my autowired fields are not detecting the beans. How do I get my #Configuration class to run before the application starts?
I tried annotating the classes with the Autowired fields as #ContextConfiguration(classes = Config.class), but this didn't work.
My spring configuration file:
#Configuration
public class Config {
#Bean
public AmazonDynamoDB amazonDynamoDB() {
return ...
}
#Bean
public DynamoDBMapper dynamoDBMapper(AmazonDynamoDB amazonDynamoDB) {
return ...
}
}
I expect the Configuration file to be run and the beans injected, but the beans are not being detected.
There's no main method, since I'm writing this in a service which is created using dependency injection in another service. I'm not sure where I'd tell my application to use my Config file.
probably place a #EnableConfigurationProperties( {Config.class}) above your #SpringBootApplication main class.

Modify the bean created in main application context during Integration test

In my springboot application I am performing Integration tests using the following class
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = AccountLoadApplication.class,
loader = SpringApplicationContextLoader.class)
#WebIntegrationTest(randomPort = true)
public class LoaderTest {
AccountLoadApplication.class is a spring boot main class and the actual application has a bean defined like below:
#Bean
public ResourceLoader recapMvsFileResourceLoader() {
return new RemoteFileResourceLoader(remoteHostProperties(), new SFTPRemoteFileService());
}
Also I have a Test Configuration class like below
#Configuration
public class AtddTestConfig {
#Bean
public ResourceLoader mvsFileResourceLoader() {
ResourceLoader recapMvsFileResourceLoader =
new RemoteFileResourceLoader(remoteHostProperties(), new FakeSFTPRemoteFileService());
return recapMvsFileResourceLoader;
}
My Idea is that I want to override the bean created in the main application using the new bean defined in the test Configuration file.
But during integration tests the main application bean is considered instead of the bean defined in the test application context?
Is There any other way to achieve what i am trying to achieve ?
Additional Info:
Here are the beans defined in my Application configuration class
#Bean
public RemoteFileService remoteFileService() {
return new SFTPRemoteFileService();
}
#Bean
public ResourceLoader recapMvsFileResourceLoader() {
return new RemoteFileResourceLoader(remoteHostProperties(), remoteFileService());
}
Here are the beans defined in my Test configuration class
#Bean
#Profile("local")
#Primary
public RemoteFileService remoteFileService() {
return new FakeSFTPRemoteFileService();
}
Still the production bean is only created instead of this primary bean.
Use #Profile annotation to enable testing bean only in test context
Use #Primary annotation on testing bean, so that spring would use test bean instead of production one.
Here is my Github repository with working example using this mechanism.
Maybe when you add your test configuration as parameter for #ContextConfiguration it resolves problem, e.g.
#ContextConfiguration(classes = {AccountLoadApplication.class, AtddTestConfig.class},
loader = SpringApplicationContextLoader.class)
Along with the other changes suggested by #luboskrnac, you have to declare #ActiveProfiles; otherwise, your local profile is simply ignored.
#RunWith(SpringJUnit4ClassRunner.class)
#ActiveProfiles("local")
#SpringApplicationConfiguration(AccountLoadApplication.class)
#WebIntegrationTest(randomPort = true)
public class LoaderTest { /* ... */ }
Note that the above assumes that your AtddTestConfig class gets picked up via component scanning by your AccountLoadApplication class.

Resources