Search code examples
spring-bootspring-mvckotlinjunitmockk

Why my org.springframework.test.web.servlet.MockMvc framework is failing to resolve constructor parameters of a controller


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:

  1. 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))
    }
}
  1. 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: