how to approach Spring boot constructor based dependency injection confusion/frustration when unit testing a component class with several dependencies - spring-boot

I am trying to write unit tests for a Spring boot service using JUnit 4 and Mockito.
I used constructor based dependency injection for my service, The Signature is:
class VbServiceImp(val jdbcTemplate: NamedParameterJdbcTemplate,
val nuanceService: NuanceService,
val conf: AppConfigProps,
val eventService: EventServiceImp,
val audioTrimService: AudioTrimServiceIF,
val vbNuanceStagingDeletionsService: VbNuanceStagingDeletionsService) : VbService {...}
in another part of the application This service gets injected into a controller and somehow spring just magically knows what to inject without me specifying this (Any idea how this works/explanation would be appreciated, guess it's based on component scan?)
example:
class VbController(val vbService: VbService) {...}
Now in my VBServiceImpl Unit test class I try to mock all the above dependencies before declaring vBService in order to manually inject all dependencies into VBService during declaration.
the relevant part of my test class looks like this:
#RunWith(SpringRunner::class)
#ContextConfiguration()
class VBServiceTests {
#MockBean
val jdbcTemplate: NamedParameterJdbcTemplate = mock()
#MockBean
val nuanceService: NuanceService = mock()
#MockBean
val appconfigProps: AppConfigProps = AppConfigProps()
#MockBean
val eventService: EventServiceImp = mock()
#MockBean
val audioTrimService: AudioTrimService = mock()
#MockBean
val vbNuanceStagingDeletionsService: VbNuanceStagingDeletionsService = mock()
val vbService: VbServiceImp = VbServiceImp(jdbcTemplate, nuanceService, appconfigProps, eventService, audioTrimService, vbNuanceStagingDeletionsService)
#SpyBean
val vbServiceSpy: VbServiceImp = Mockito.spy(vbService)
#Before
fun setup() {
initMocks(this)
}
When I run a test I get the exception below. If I understand this correctly there is already a bean of type jdbcTemplate in the application context and therefore I can't define the #Mockbean jdbcTemplate above?
exception:
private final org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate com.cc.ff.vb.service.VBServiceTests.jdbcTemplate cannot have an existing value
So now the issue is: If I removed the #MockBean jdbcTemplate variable then I can't inject jdbcTemplate when I declare vbService in my test class. So how could I get around this/make this work?
Just to check I removed the jdbcTemplate parameter from the vbService class constructor and changed it to a #Autowired field injected class variable and provided the mock class using #TestConfig. This worked however then the exception popped up on the next constructor parameter (NuanceService)
i'm out of ideas and google hasn't returned anything of value. Do I remove all constructor injected dependencies and then make them field injected using #Autowired and then provide the beans in the nested #TestConfig annotated class or is there a better/cleaner way? AFAIK field based injection is supposed to be bad practice?
example of providing correct bean for testing #Autowired field injected jdbcTemplate variable:
#TestConfiguration
class testConfig {
#Bean
fun jdbcTemplate(): NamedParameterJdbcTemplate {
return mock<NamedParameterJdbcTemplate>()
}
}

This is how I ended up making it work:
#TestConfiguration
class testConfig {
#Bean
fun jdbcTemplate(): NamedParameterJdbcTemplate {
return mock<NamedParameterJdbcTemplate>()
}
#Bean
fun nuanceService(): NuanceService {
return mock<NuanceService>()
}
#Bean
fun appConfigProps(): AppConfigProps {
return mock<AppConfigProps>()
}
#Bean
fun eventService(): EventServiceImp {
return mock<EventServiceImp>()
}
#Bean
fun audioTrimService(): AudioTrimService {
return mock<AudioTrimService>()
}
#Bean
fun vbNuanceStagingDeletionService(): VbNuanceStagingDeletionsService {
return mock<VbNuanceStagingDeletionsService>()
}
}
#MockBean
lateinit var nuanceService: NuanceService
#SpyBean
lateinit var vbServiceSpy: VbServiceImp
I'm still not sure if this is the best/optimal way of going about this so would appreciate some more details...

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")}
}
}
}

Mockito mock does not work as expected in Spring MockMvc test

In an Spring mockmvc test I want to replace a bean by a mock implementation which is configured using Mockito.when() definitions. The definitions are indeed respected at the time the mock is configured, as well as at the time the mock is injected into a depending bean (a controller advice in my case) during application context startup. However, when the mock is used during a certain test, all when definitions are gone.
Why?
Some remarks:
The mock is completely new code, so it is impossible that I am not aware of any call to Mockito.reset().
the mock at the time of usage is the same as at the time of creation.
a bypassing solution to the problem is to configure the mock in a #BeforeEach method in AbstractTest. However, I want to understand why it does not work without.
Here a simplified and anonymized example
#Component
public class MyBean {
private String property;
...
public String getProperty() {
return property;
}
}
#ControllerAdvice
public class MyControllerAdvice() {
private MyBean myBean;
#Autowired
public MyControllerAdvice(MyBean myBean) {
this.myBean = myBean;
System.out.println(this.myBean.getProperty()); // --> outputs "FOOBAR"
}
#ModelAttribute
public String getMyBeanProperty() {
return myBean.getProperty(); // --> returns null
}
}
public class AbstractTest {
#Configuration
static class Config {
#Bean
public MyBean () {
MyBean myBean = Mockito.mock(MyBean.class, "I am a mock of MyBean");
when(myBean.getProperty()).thenReturn("FOOBAR");
}
}
}
That's not a problem of Mockito. I think you simplified the example a lot and we don't see the full picture, but I can say that main cause - 2 different beans MyBean: one is initialized with Spring's #Component, second is in configuration class with #Bean.
Why do you use #Component for POJO/DO?
#Bean in the configuration class is being initialized lazy so better way to use #PostConstruct
If you want to leave both beans mark MyBean in the configuration class as #Primary

Autowire Spring TestEntityManager in Kotlintest not working

I am, as a learning experience, trying to port the tests described in Spring Boot's kotlin tutorial (https://spring.io/guides/tutorials/spring-boot-kotlin/) from using JUnit 5 to KotlinTest.
In a repositories test annotated with #DataJpaTest, I manage to autowire my JPA repositories fine. But autowiring Spring Boot's TestEntityManager does not work, as I get java.lang.IllegalStateException: No transactional EntityManager found when I try to use it in a test.
In addition to #DataJpaTest, I tried adding #Transactional, without luck.
When running the test with JUnit5, persisting an entity with the entityManager works fine.
#DataJpaTest
class RepositoriesTests #Autowired constructor(
val entityManager: TestEntityManager,
val userRepository: UserRepository,
val articleRepository: ArticleRepository) {
#Test
fun `When findByIdOrNull then return article`() {
val juergen = User(login = "springjuergen", firstname = "Juergen", lastname = "Hoeller")
entityManager.persist(juergen) <--- OK
But with KotlinTest, it fails:
#DataJpaTest
class RepositoriesTests #Autowired constructor(
val entityManager: TestEntityManager,
val userRepository: UserRepository,
val articleRepository: ArticleRepository) : StringSpec() {
init {
"When findByIdOrNull then return article" {
val juergen = User(login = "springjuergen", firstname = "Juergen", lastname = "Hoeller")
entityManager.persist(juergen) <--- FAIL
The error being java.lang.IllegalStateException: No transactional EntityManager found.
I haven't been able to figure out how to provide this entitymanager in the test implemented using KotlinTest.
Just create transaction wrapper, like this:
#Component
class TransactionWrapperUtils {
#Transactional
public fun runWithTransaction(f: () -> Unit) {
f()
}
}
And call it
transactionWrapperUtils.runWithTransaction { entityManager.persist(juergen) }

How to use spring annotations like #Autowired in kotlin?

Is it possible to do something like following in Kotlin?
#Autowired
internal var mongoTemplate: MongoTemplate
#Autowired
internal var solrClient: SolrClient
Recommended approach to do Dependency Injection in Spring is constructor injection:
#Component
class YourBean(
private val mongoTemplate: MongoTemplate,
private val solrClient: SolrClient
) {
// code
}
Prior to Spring 4.3 constructor should be explicitly annotated with Autowired:
#Component
class YourBean #Autowired constructor(
private val mongoTemplate: MongoTemplate,
private val solrClient: SolrClient
) {
// code
}
In rare cases, you might like to use field injection, and you can do it with the help of lateinit:
#Component
class YourBean {
#Autowired
private lateinit var mongoTemplate: MongoTemplate
#Autowired
private lateinit var solrClient: SolrClient
}
Constructor injection checks all dependencies at bean creation time and all injected fields is val, at other hand lateinit injected fields can be only var, and have little runtime overhead. And to test class with constructor, you don't need reflection.
Links:
Documentation on lateinit
Documentation on constructors
Developing Spring Boot applications with Kotlin
Yes, java annotations are supported in Kotlin mostly as in Java.
One gotcha is annotations on the primary constructor requires the explicit 'constructor' keyword:
From https://kotlinlang.org/docs/reference/annotations.html
If you need to annotate the primary constructor of a class, you need to add the constructor keyword to the constructor declaration, and add the annotations before it:
class Foo #Inject constructor(dependency: MyDependency) {
// ...
}
You can also autowire dependencies through the constructor. Remember to annotate your dependencies with #Configuration, #Component, #Service etc
import org.springframework.stereotype.Component
#Component
class Foo (private val dependency: MyDependency) {
//...
}
like that
#Component class Girl( #Autowired var outfit: Outfit)
If you want property injection but don't like lateinit var, here is my solution using property delegate:
private lateinit var ctx: ApplicationContext
#Component
private class CtxVarConfigurer : ApplicationContextAware {
override fun setApplicationContext(context: ApplicationContext) {
ctx = context
}
}
inline fun <reified T : Any> autowired(name: String? = null) = Autowired(T::class.java, name)
class Autowired<T : Any>(private val javaType: Class<T>, private val name: String?) {
private val value by lazy {
if (name == null) {
ctx.getBean(javaType)
} else {
ctx.getBean(name, javaType)
}
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
}
Then you can use the much better by delegate syntax:
#Service
class MyService {
private val serviceToBeInjected: ServiceA by autowired()
private val ambiguousBean: AmbiguousService by autowired("qualifier")
}

Spring HATEOAS Resource assembler is not instantiated in unit test

I am trying to write a unit test for a REST controller which generates HATEOAS links via Resource assembler class. Everything is OK in production, but with the unit test Resource assembler class is not being injected into the controller.
my resource assembler class is:
#Component
public class ModelResourceAssembler extends ResourceAssemblerSupport<Model, ModelResource> {
public ModelResourceAssembler() {
super(ModelRestController.class, ModelResource.class);
}
#Bean
public ModelResourceAssembler modelResourceAssembler(){
return new ModelResourceAssembler();
}
#Override
public ModelResource toResource(Model model) {
...
}
}
The controller is:
#Controller
#RequestMapping("/demo")
#ComponentScan(basePackages = {"com.foo.demo"} )
public class ModelRestController {
#Autowired
private ModelPersistenceHandler modelPersistenceHandler;
#Autowired
private ModelResourceAssembler modelResourceAssembler;
...
}
And the unit test:
#RunWith(MockitoJUnitRunner.class)
#ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes= {ModelResourceAssembler.class, ModelRestController.class})
public class ModelRestControllerTest {
private MockMvc mockMvc;
#InjectMocks
private ModelRestController modelRestController;
#Mock
private ModelPersistenceHandler modelPersistenceHandler;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(modelRestController).build();
}
...
}
No matter what I do the ModelResourceAssembler instance is always null. Since the application is Spring Boot it does not have the WebCoonfig classes and autowired WebApplicationContext is always null, so I cannot (and really don't want to since I am running a unit test) instantiate MockMvc via webAppContextSetup
The solution ended up being quite simple: I needed to add one line to my test:
#Spy
private ModelResourceAssembler modelResourceAssembler;
And the bean was instantiated and properly wired
In your example you use #InjectMocks but don't declare a mock for ModelResourceAssembler. You don't get an instance out of nowhere.
You use the MockitoJUnitRunner.class. It has no idea of Spring beans. For testing Spring applications you rather want to use SpringJUnit4ClassRunner.class.
If i may suggest, if you use constructor injection for your controller then you can just mock the dependency and not need spring junit test runner stuff.

Resources