Как запустить @Bean с пользовательскими параметрами после события, произошедшего с spring для тестов?

#kotlin #javabeans #applicationcontext #testcontainers

Вопрос:

Я работаю над добавлением тестов репозиториев с помощью платформы TestContainers для проекта, в котором используется R2dbc, и я сталкиваюсь со следующей ситуацией:

1 — В основном проекте я установил r2dbc URL-адрес (с портом и именем хоста) в приложении.файл yaml и данные spring управляют всем, и все просто работает.

2 — Однако в тестах я использую платформу TestContainers, более конкретно DockerComposeContainer , которую я использую для создания поддельного контейнера с помощью compose.test.yaml файла docker с необходимыми мне базами данных.

3 — Этот контейнер создает port номер на ходу Я определяю номер порта в своем файле docker-compose, но номер порта, который DockerComposeContainer мне будет предоставлен, является случайным и меняется каждый раз, когда я запускаю тесты, что делает статический URL application-test.yaml -адрес больше не вариантом.

Поэтому мне нужно динамически создать этот компонент R2dbcEntityTemplate во время выполнения и только после DockerComposeContainer того, как он сообщит мне номер порта. Таким образом, мое приложение может подключиться к правильному порту, и все должно работать так, как ожидалось.

Я пытался создать этот класс:

     package com.wayfair.samworkgroupsservice.adapter

    import io.r2dbc.mssql.MssqlConnectionConfiguration
    import io.r2dbc.mssql.MssqlConnectionFactory
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.beans.factory.config.ConstructorArgumentValues
    import org.springframework.beans.factory.support.BeanDefinitionRegistry
    import org.springframework.beans.factory.support.GenericBeanDefinition
    import org.springframework.context.ApplicationContext
    import org.springframework.context.annotation.Profile
    import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy
    import org.springframework.data.r2dbc.core.R2dbcEntityTemplate
    import org.springframework.data.r2dbc.dialect.SqlServerDialect
    import org.springframework.r2dbc.core.DatabaseClient
    import org.springframework.stereotype.Component
    
    @Component
    @Profile("test")
    class TemplateFactory(
        @Autowired val applicationContext: ApplicationContext
    ) {
        private val beanFactory = applicationContext.autowireCapableBeanFactory as BeanDefinitionRegistry
    
        fun registerTemplateBean(host: String, port: Int) {
            val beanDefinition = GenericBeanDefinition()
            beanDefinition.beanClass = R2dbcEntityTemplate::class.java
            val args = ConstructorArgumentValues()
            args.addIndexedArgumentValue(
                0,
                DatabaseClient.builder()
                    .connectionFactory(connectionFactory(host, port))
                    .bindMarkers(SqlServerDialect.INSTANCE.bindMarkersFactory)
                    .build()
            )
            args.addIndexedArgumentValue(1, DefaultReactiveDataAccessStrategy(SqlServerDialect.INSTANCE))
    
            beanDefinition.constructorArgumentValues = args
            beanFactory.registerBeanDefinition("R2dbcEntityTemplate", beanDefinition)
        }
    
    //    fun entityTemplate(host: String = "localhost", port: Int = 1435) =
    //        R2dbcEntityTemplate(
    //            DatabaseClient.builder()
    //                .connectionFactory(connectionFactory(host, port))
    //                .bindMarkers(SqlServerDialect.INSTANCE.bindMarkersFactory)
    //                .build(),
    //            DefaultReactiveDataAccessStrategy(SqlServerDialect.INSTANCE)
    //        )
    
        private fun connectionFactory(host: String, port: Int) =
            MssqlConnectionFactory(
                MssqlConnectionConfiguration.builder()
                    .host(host)
                    .port(port)
                    .username("sa")
                    .password("Password123@#?")
                    .build()
            )
    }
 

And this is how my db initiliser looks like:

     package com.wayfair.samworkgroupsservice.adapter.note

    import com.wayfair.samworkgroupsservice.adapter.DBInitializerInterface
    import com.wayfair.samworkgroupsservice.adapter.TemplateFactory
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.data.r2dbc.core.R2dbcEntityTemplate
    import org.testcontainers.containers.DockerComposeContainer
    import org.testcontainers.containers.wait.strategy.Wait
    import org.testcontainers.junit.jupiter.Container
    import org.testcontainers.junit.jupiter.Testcontainers
    import java.io.File
    
    @Testcontainers
    class NoteTagDBInitializer : DBInitializerInterface {
        @Autowired
        override lateinit var client: R2dbcEntityTemplate
    
        @Autowired
        lateinit var factory: TemplateFactory
    
        override val sqlScripts = listOf(
            "db/note/schema.sql",
            "db/note/reset.sql",
            "db/note/data.sql"
        )
    
        init {
            factory.registerTemplateBean(
                cont.getServiceHost("test-db-local_1", 1433),
                cont.getServicePort("test-db-local_1", 1433)
            )
        }
    
        companion object {
            @Container
            val cont: KDockerComposerContainer = KDockerComposerContainer("docker-compose.test.yml")
                .withExposedService(
                    "test-db-local_1", 1433,
                    Wait.forListeningPort()
                )
                .withLocalCompose(true)
                .also {
                    it.start()
                    val porttt = it.getServicePort("test-db-local_1", 1433)
                    print(porttt)
                }
    
            class KDockerComposerContainer(yamlFile: String) :
                DockerComposeContainer<KDockerComposerContainer>(File(yamlFile))
        }
    }
 

Я не получаю ошибок при попытке запустить эту фабрику шаблонов без полезного сообщения об ошибке,
Но, честно говоря, я больше не знаю, прилагаю ли я усилия для правильного решения, есть ли у кого-нибудь какое-либо представление о том, как это сделать, или я делаю здесь что-то не так?

Итак, чтобы подвести итог для производственного приложения, все в порядке, оно запускается на основе URL-адреса приложения.файл yaml и все, но для тестов мне нужно что-то динамичное с портами, которые будут меняться каждый раз.

Заранее спасибо ))

Ответ №1:

У Spring уже есть решение вашей проблемы. Если вы используете довольно свежую версию Spring (>= > 5.2.5 ), вам следует использовать @DynamicPropertySource для настройки свойств конфигурации теста динамическое значение порта базы данных контейнера. Прочитайте официальную документацию spring для получения более подробной информации и примеров кода kotlin.

Если вы застряли со старой версией Spring, вам нужен интерфейс ApplicationContextInitializer . Смотрите этот весенний выпуск github для небольшого примера.