1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SpringのBean Definition DSL で登録したComponent をテストで使用する方法

Last updated at Posted at 2022-05-27

Bean Definition DSL とは

https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#kotlin-bean-definition-dsl
Spring の Kotlinサポート機能で、SpringコンポーネントをDSLで登録する方法です。
従来の @Component などのように宣言的にコンポーネント登録せず、明示的にコンポーネントを登録していきます。

  • 従来のコンポーネント登録
@Configuration
class Config {
    @Bean
    fun myHandler() = MyHandler()
}

とか

  • 従来のコンポーネント登録その2
@Component
class MyHandler {
    fun findAll(req: ServerRequest) = ok().body("hello", String::class.java)
}

といったいつものやつに対して、Bean Definition DSLはアノテーションを使用せず

  • Bean Definition DSL によるコンポーネント登録
@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args) {
        addInitializers(beans);
    }
}

private val beans: BeanDefinitionDsl = beans {
    bean<MyHandler>()
}

こんな感じで、 アノテーションではなく、 bean<型>() として明示的に(?)コンポーネントを登録していきます。
addInitializers(beans) これで実際にContextに対してBean登録されてます。

このやり方はコンポーネントスキャンベースと比較して起動速度に関しては優位でもなさそうなのであまりメリットはわからないです。
https://towardsdatascience.com/a-benchmark-of-spring-annotations-vs-kotlin-dsl-209f54294325

  • RouterFunctionDSL との組み合わせ
@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args) {
        addInitializers(beans);
    }
}

private val beans: BeanDefinitionDsl = beans {
    bean<MyHandler>()
    bean { route(ref()) }
}

private fun route(myHandler: MyHandler): RouterFunction<ServerResponse> = router {
    GET("/", myHandler::findAll)
}

とかDSLの組み合わせで小規模なAPIサーバーをサクッと作れるのが楽なのかもしれないです。

困ったこと

事象

テストでBeanが登録されない!!

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApplicationTests(
    private val webTestClient: WebTestClient,
) : FunSpec({
    test("access top") {
        webTestClient
            .get()
            .uri("/")
            .accept(MediaType.APPLICATION_JSON)
            .exchange()
            .expectStatus().isOk
    }

こんなtestを実行しても、404になりテストが通りませんでした。
route 関数で設定した RouterFunction がコンポーネントとして登録されていなそうでした。

@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args) {
        addInitializers(beans);
    }
}

private val beans: BeanDefinitionDsl = beans {
    bean<MyHandler>()
    bean { route(ref()) }
}

private fun route(myHandler: MyHandler): RouterFunction<ServerResponse> = router {
    GET("/", myHandler::findAll)
}

アノテーションベースのSpringTestならいい感じにコンポーネント登録してくれているので、頭がフリーズします。

原因

fun main(args: Array<String>) {
    runApplication<Application>(*args) {
        addInitializers(beans);
    }
}

メイン関数内で addInitializers(beans: BeanDefinitionDsl) してるからでした。
SpringExtension はプロダクションコードのメイン関数を呼び出しているわけではないので、当たり前といえば当たり前でした。

解決方針

テストコードもプロダクションコードと同じコンポーネントを指定

プロダクションコードがテストコードと同じコンポーネントを指定する必要があるなら、Beanを外部ファイルも読み取れるスコープに配置し
テストコードは @ContextConfiguration で initializers に指定すればよい
ただし、 @ContextConfiguration.initializers が求める型は Class<? extends ApplicationContextInitializer<?>>[] なので、

val beans = beans {
    bean<MyHandler>()
    bean { route(ref()) }
}

と書かれた beans を直接参照することはできない。

仕方がないので

class MyContextInitializer : ApplicationContextInitializer<GenericApplicationContext> {
    override fun initialize(applicationContext: GenericApplicationContext) {
        beans.initialize(applicationContext)
    }
}

private val beans: BeanDefinitionDsl = beans {
    bean<MyHandler>()
    bean { route(ref()) }
}

fun route(myHandler: MyHandler): RouterFunction<ServerResponse> = router {
    GET("/", myHandler::findAll)
}

Bean Definition DSL を context に登録するような ApplicationContextInitializer を実装します。

  • プロダクションコード

@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args) {
        addInitializers(MyContextInitializer());
    }
}
  • テストコード
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = [MyContextInitializer::class])
class ApplicationTests(

とすることで無事にプロダクションとテストで同じコンポーネントを生成できました。

疑問

「テストコードの一部コンポーネントのみ動作を変えたbeanをinjectしたい。」みたいなニーズってあると思うのですが、その場合はどうするのが正しいかがわからないです。
アノテーションベースなら @Conditional みたいなアノテーションで1発にも思えるのですが、なにかうまいやり方があるのか。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?