Issue
I want to create a unit tests for my Spring Boot app, but I receive following error during launching one of the UserControllerTest
test:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.sample.order.OrderController required a bean of type 'com.sample.order.OrderRepository' that could not be found.
Action:
Consider defining a bean of type 'com.sample.order.OrderRepository' in your configuration.
When I'm trying to run tests from UserControllerTest
, then OrderController
fails to satisfy constructor parameters and vice versa.
The app is simple and has only two controllers, for managing orders and users. The source code has following structure:
src/
├── main
│ ├── kotlin
│ │ └── com
│ │ └── sample
│ │ ├── OrderApplication.kt
│ │ ├── order
│ │ │ ├── Order.kt
│ │ │ ├── OrderAssembler.kt
│ │ │ ├── OrderController.kt
│ │ │ └── OrderRepository.kt
│ │ └── user
│ │ ├── User.kt
│ │ ├── UserAssembler.kt
│ │ ├── UserController.kt
│ │ └── UserRepository.kt
│ └── resources
│ └── application.properties
└── test
└── kotlin
└── com
└── sample
├── order
│ └── OrderControllerTest.kt
└── user
└── UserControllerTest.kt
In general for building my app I was trying to follow the Spring Boot kotlin tutorial.
The app itself runs without any problem. The problem lies when running tests.
I see that the action proposed by the framework is quite obvious:
Consider defining a bean of type 'com.sample.order.OrderRepository' in your configuration.
, but why should I define a bean of type 'com.sample.order.OrderRepository' in configuration ? Shouldn't test be working out of the box when the app itself works and does not need any configuration?
Besides this the OrderRepository
is type of interface:
interface OrderRepository: CrudRepository<Order, Long>
and I do not want to provide instantiation of this when Spring does its magic.
What I tried:
1. Changing constructors of controllers to accept nullable parameters eg.:
From:
class OrderController(private val repository: OrderRepository
,private val assembler: OrderAssembler) {
...
To:
class OrderController(private val repository: OrderRepository? = null
,private val assembler: OrderAssembler? = null) {
...
It resolves the issue, but I do not want to have this solution from obvious reasons.
2. Adding 'order' mocked variables to 'user' unit test
class UserControllerTest(@Autowired val mockMvc: MockMvc) {
@MockkBean
private lateinit var userRepository: UserRepository
@SpykBean
private lateinit var userAssembler: UserAssembler
// !!!!!! NOTE: orderRepository inside user tests ¯\_(ツ)_/¯
@MockkBean
private lateinit var orderRepository: OrderRepository
// !!!!!! NOTE: orderAssembler inside user tests ¯\_(ツ)_/¯
@SpykBean
private lateinit var orderAssembler: OrderAssembler
@Test
fun `Should form expected json response with user data`(){
val user = User(id = 5, login="[email protected]", firstname = "John", lastname = "Doe")
every { userRepository.findAll() } returns listOf(user)
mockMvc.perform(get("/api/v1/users/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
}
}
It also resolves the issue, but it is ugly workaround.
3. Setting explicit package name in ComponentScan
@SpringBootApplication
@ComponentScan("com.sample.order")
class OrderApplication
fun main(args: Array<String>) {
runApplication<OrderApplication>(*args)
}
This does not fix the issue.
The code of the UserController:
@RestController
class UserController(private val repository: UserRepository,
private val assembler: UserAssembler) {
@GetMapping("/api/v1/user/{id}")
fun findOne(@PathVariable id: Long): EntityModel<User> {
val user = repository.findById(id)
if (user.isPresent) {
return assembler.toModel(user.get())
}
throw ResponseStatusException(HttpStatus.NOT_FOUND, "This user does not exist")
}
@GetMapping("/api/v1/users")
fun all(): CollectionModel<EntityModel<User>> {
val users = repository.findAll().map {
assembler.toModel(it)
}
return CollectionModel.of(users, //
linkTo<UserController>{all()}.withRel("all"))
}
}
It does not help. Besides this my app can be run without any problem, so I think that component scanning is not a reason.
Versions used:
- Java: 14
- Kotlin: 1.4.0
- Spring Boot: 2.4.1
- springmockk: 1.1.3
- mockk:1.10.5
Below is full call stack:
Failed to resolve parameter [org.springframework.test.web.servlet.MockMvc mockMvc] in constructor [public com.sample.user.UserControllerTest(org.springframework.test.web.servlet.MockMvc)]
org.junit.jupiter.api.extension.ParameterResolutionException: Failed to resolve parameter [org.springframework.test.web.servlet.MockMvc mockMvc] in constructor [public com.sample.user.UserControllerTest(org.springframework.test.web.servlet.MockMvc)]
at org.junit.jupiter.engine.execution.ExecutableInvoker.resolveParameter(ExecutableInvoker.java:221)
at org.junit.jupiter.engine.execution.ExecutableInvoker.resolveParameters(ExecutableInvoker.java:174)
at org.junit.jupiter.engine.execution.ExecutableInvoker.resolveParameters(ExecutableInvoker.java:135)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:61)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.invokeTestClassConstructor(ClassTestDescriptor.java:342)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.instantiateTestClass(ClassTestDescriptor.java:289)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.instantiateTestClass(ClassTestDescriptor.java:281)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.instantiateAndPostProcessTestInstance(ClassTestDescriptor.java:269)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$testInstanceProvider$2(ClassTestDescriptor.java:259)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$testInstanceProvider$3(ClassTestDescriptor.java:263)
at java.base/java.util.Optional.orElseGet(Optional.java:369)
at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$testInstanceProvider$4(ClassTestDescriptor.java:262)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:82)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:59)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$1(NodeTestTask.java:111)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:111)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:79)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$5(DefaultLauncher.java:211)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at com.sun.proxy.$Proxy2.stop(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:133)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:123)
at org.springframework.test.context.junit.jupiter.SpringExtension.getApplicationContext(SpringExtension.java:271)
at org.springframework.test.context.junit.jupiter.SpringExtension.resolveParameter(SpringExtension.java:257)
at org.junit.jupiter.engine.execution.ExecutableInvoker.resolveParameter(ExecutableInvoker.java:207)
... 74 more
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'orderController' defined in file [.../webapp/build/classes/kotlin/main/com/sample/order/OrderController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.sample.order.OrderRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1206)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:571)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:531)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:923)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:588)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:767)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:426)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:326)
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:122)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
... 78 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.sample.order.OrderRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1777)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1333)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1287)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 97 more
The tests source code:
- OrderControllerTest.kt
package com.sample.order
import com.ninjasquad.springmockk.MockkBean
import com.ninjasquad.springmockk.SpykBean
import io.mockk.every
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.util.*
@WebMvcTest
class OrderControllerTest(@Autowired val mockMvc: MockMvc) {
//Why we need that to have test compilable ?
/*
@MockkBean
private lateinit var userRepository: UserRepository
@SpykBean
private lateinit var userAssembler: UserAssembler
*/
@MockkBean
private lateinit var orderRepository: OrderRepository
@SpykBean
private lateinit var orderAssembler: OrderAssembler
@Test
fun `Should form expected json response with order data`(){
val order = Order(id = 7, date = 1501212321)
every { orderRepository.findById(any()) } returns Optional.of(order)
mockMvc.perform(get("/api/v1/order/7").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(jsonPath("$.id").value(order.id))
}
}
- UserControllerTest.kt
package com.sample.user
import com.ninjasquad.springmockk.MockkBean
import com.ninjasquad.springmockk.SpykBean
import io.mockk.every
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
@WebMvcTest
class UserControllerTest(@Autowired val mockMvc: MockMvc) {
@MockkBean
private lateinit var userRepository: UserRepository
@SpykBean
private lateinit var userAssembler: UserAssembler
/*
@MockkBean
private lateinit var orderRepository: OrderRepository
@SpykBean
private lateinit var orderAssembler: OrderAssembler
*/
@Test
fun `Should form expected json response with user data`(){
val user = User(id = 5, login = "[email protected]", firstname = "John", lastname = "Doe")
every { userRepository.findAll() } returns listOf(user)
mockMvc.perform(get("/api/v1/users").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
}
}
The source code is available on github
Solution
The solution is to change the test annotations
- Add @AutoConfigureMockMvc
- Add @SpringBootTest
- Remove @WebMvcTest
Working test annotations look like this:
@AutoConfigureMockMvc
@SpringBootTest
//@WebMvcTest
class UserControllerTest(@Autowired val mockMvc: MockMvc) {
...
@AutoConfigureMockMvc
- enables and configures auto-configuration of MockMvc
@SpringBootTest
- tells Spring Boot to go and look for a main configuration class (one with @SpringBootApplication for instance), and use that to start a Spring application context. SpringBootTest loads complete application and injects all the beans which can be slow.
@WebMvcTest
- for testing the controller layer and you need to provide remaining dependencies required using Mock Objects.
More details can be found here:
Answered By - rojarand