Search code examples
javaspring-bootkotlinspring-boot-test

how to specify nameGenerator in SpringBootTest?


I am trying to run @SpringBootTest with a subset of classes. There are 2 beans with conflicting names among these classes.

@SpringBootTest(
    classes = [BarService::class, ConflictName::class, com.foo.ConflictName::class, FooService::class]
)
class DemoApplicationTests

The test fails with BeanDefinitionOverrideException

Caused by: org.springframework.beans.factory.support.BeanDefinitionOverrideException: 
Invalid bean definition with name 'conflictName' defined in null:
Cannot register bean definition [Generic bean: class [com.foo.ConflictName]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null] 
for bean 'conflictName' since there is already [Generic bean: class [com.bar.ConflictName]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null] bound.

However, if the test is running for the whole application, without specifying the concrete classes

@SpringBootTest
class DemoApplicationTests

the execution is successful.

The question is how to specify nameGenerator in SpringBootTest similar to @SpringBootApplication(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator::class)?

the complete code example:

package com.bar

import org.springframework.stereotype.Service

@Service
class BarService(private val conflictName: ConflictName)
========================================================
package com.bar

import org.springframework.stereotype.Component

@Component
class ConflictName
========================================================
package com.foo

import org.springframework.stereotype.Component

@Component
class ConflictName
========================================================
package com.foo

import org.springframework.stereotype.Service

@Service
class FooService(private val conflictName: ConflictName)
========================================================
package com

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator

@SpringBootApplication(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator::class)
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

Solution

  • To achieve the same as @SpringBootApplication(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator::class) in a (isolated configuration) test, we can:

    • declare an "internal" @Configuration class (which will skip/replace spring boot application loading... to "add/modify" (loaded) spring boot application, use @TestConfiguration (internal class)).
    • use @ComponentScan on that, which:
      • takes a nameGenerator:Class<...> parameter (, which exactly is "aliased" by @SpringBootApplication).
      • if omit, it falls back to "current package", but if we use basePackageClasses, we can cherry pick our components. (But please note it is (as in @SpringBootApplication) package- not class-wise!)

    A sample test:

    package com
    
    import org.junit.jupiter.api.Assertions
    import org.junit.jupiter.api.Test
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.boot.test.context.SpringBootTest
    import org.springframework.context.annotation.ComponentScan
    import org.springframework.context.annotation.Configuration
    import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator
    
    @SpringBootTest
    class DemoApplicationTests {
        @Configuration // only this will be used for this test class!
        @ComponentScan(
          nameGenerator = FullyQualifiedAnnotationBeanNameGenerator::class,
          basePackageClasses = [com.foo.ConflictName::class, com.bar.ConflictName::class]
        )
        // empty/customize:
        internal class IsolatedTestConfig 
    
        @Autowired(required = false)
        var springBootApp: org.springframework.boot.SpringApplication? = null
    
        @Autowired(required = false)
        var compFoo: com.foo.ConflictName? = null
    
        @Autowired(required = false)
        var compBar: com.bar.ConflictName? = null
    
        @Test
        fun testNamingAndIsolation() {
            Assertions.assertNull(springBootApp) // !
            Assertions.assertNotNull(compFoo)
            Assertions.assertNotNull(compBar)
        }
    }