I want to write integration test for my microservice currently using Kotlin, Jooq and R2dbc at repository level.
I want my test to work in R2dbc mode as well, but for some reason getting this exception:
Caused by: org.testcontainers.containers.JdbcDatabaseContainer$NoDriverFoundException: Could not get Driver
at org.testcontainers.containers.JdbcDatabaseContainer.getJdbcDriverInstance(JdbcDatabaseContainer.java:187)
at org.testcontainers.containers.JdbcDatabaseContainer.createConnection(JdbcDatabaseContainer.java:209)
at org.testcontainers.containers.JdbcDatabaseContainer.waitUntilContainerStarted(JdbcDatabaseContainer.java:147)
at org.testcontainers.containers.GenericContainer.tryStart(GenericContainer.java:466)
... 10 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
Probably, I have to point somewhere that I want to use r2dbc only, not jdbc? I've seen the specs but not sure whether I applied TC_INITSCRIPT and TC_IMAGE_TAG correctly.
I don't use Spring Data r2dbc (jooq only), that's why ResourceDatabasePopulator is not an option for me.
My test looks like:
#SpringBootTest(classes = [UserServiceApp::class])
#ActiveProfiles(profiles = ["test"])
#AutoConfigureWebTestClient
class UserServiceAppIT(#Autowired val client: WebTestClient) {
#Nested
inner class Find {
#Test
#DisplayName("Find existing user by id")
fun `existing user credentials returns OK`() {
val expectedUser = getCredentialsUser() //this is a class with expected data
val response = client.get()
.uri("/user/2") //this is my endpoint
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk
.expectBody(UserCredentialsModel::class.java)
.returnResult()
.responseBody
assertThat(response)
.isNotNull
.isEqualTo(expectedUser)
}
}
Test config in yaml file:
server.port: 8080
spring:
application:
name: User Service Test
r2dbc:
url: r2dbc:tc:mysql:///pharmacy?TC_IMAGE_TAG=8.0.26&TC_INITSCRIPT=classpath/resources/init.sql
password: root
username: root
pool:
initial-size: 1
max-size: 10
max-idle-time: 30m
Dependencies (gradle):
buildscript {
ext {
springDependencyVersion = '1.0.11.RELEASE'
springBootVersion = '2.5.3'
kotlinVersion = '1.5.0'
jooqPluginVersion = '6.0'
springdocVersion = '1.5.10'
r2dbcMySQLVersion = '0.8.2.RELEASE'
r2dbcPoolVersion = '0.8.7.RELEASE'
mockKVersion = '1.12.0'
kotestVersion = '4.4.3'
kotlinJsonVersion = '1.2.1'
kotlinDateVersion = '0.2.1'
testcontainersVersion = '1.16.0'
}
}
It is easy to integrate Jooq with R2dbc.
#Configuration
class JooqConfig {
#Bean
fun dslContext(connectionFactory: ConnectionFactory) =
using(TransactionAwareConnectionFactoryProxy(connectionFactory), SQLDialect.POSTGRES)
}
NOTE: Do not include Jooq starter if you are using Spring 2.7.x. The Jooq autoconfiguration only supports Jdbc.
An example using Jooq.
class PostRepositoryImpl(private val dslContext: DSLContext) : PostRepositoryCustom {
override fun findByKeyword(title: String): Flow<PostSummary> {
val sql = dslContext
.select(
POSTS.ID,
POSTS.TITLE,
field("count(comments.id)", SQLDataType.BIGINT)
)
.from(
POSTS
.leftJoin(COMMENTS.`as`("comments"))
.on(COMMENTS.POST_ID.eq(POSTS.ID))
)
.where(
POSTS.TITLE.like("%$title%")
.and(POSTS.CONTENT.like("%$title%"))
.and(COMMENTS.CONTENT.like("%$title%"))
)
.groupBy(POSTS.ID)
return Flux.from(sql)
.map { r -> PostSummary(r.value1(), r.value2(), r.value3()) }
.asFlow();
}
override suspend fun countByKeyword(title: String): Long {
val sql = dslContext
.select(
DSL.field("count(distinct(posts.id))", SQLDataType.BIGINT)
)
.from(
POSTS
.leftJoin(COMMENTS.`as`("comments"))
.on(COMMENTS.POST_ID.eq(POSTS.ID))
)
.where(
POSTS.TITLE.like("%$title%")
.and(POSTS.CONTENT.like("%$title%"))
.and(COMMENTS.CONTENT.like("%$title%"))
)
return Mono.from(sql).map { it.value1() ?: 0 }.awaitSingle()
}
}
TestContainers database requires a Jdbc driver, add MySQL Jdbc driver with testcontainter into your test scope.
The following is an example using Postgres and Testcontainers.
#OptIn(ExperimentalCoroutinesApi::class)
#Testcontainers
#DataR2dbcTest()
#Import(JooqConfig::class, R2dbcConfig::class)
class PostRepositoriesTest {
companion object {
private val log = LoggerFactory.getLogger(PostRepositoriesTest::class.java)
#Container
val postgreSQLContainer = PostgreSQLContainer("postgres:12")
.withCopyFileToContainer(
MountableFile.forClasspathResource("/init.sql"),
"/docker-entrypoint-initdb.d/init.sql"
)
#JvmStatic
#DynamicPropertySource
fun registerDynamicProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.r2dbc.url") {
"r2dbc:postgresql://${postgreSQLContainer.host}:${postgreSQLContainer.firstMappedPort}/${postgreSQLContainer.databaseName}"
}
registry.add("spring.r2dbc.username") { postgreSQLContainer.username }
registry.add("spring.r2dbc.password") { postgreSQLContainer.password }
}
}
#Autowired
lateinit var postRepository: PostRepository
#Autowired
lateinit var dslContext: DSLContext
#BeforeEach
fun setup() = runTest {
log.info(" clear sample data ...")
val deletedPostsCount = Mono.from(dslContext.deleteFrom(POSTS)).awaitSingle()
log.debug(" deletedPostsCount: $deletedPostsCount")
}
#Test
fun `query sample data`() = runTest {
log.debug(" add new sample data...")
val insertPostSql = dslContext.insertInto(POSTS)
.columns(POSTS.TITLE, POSTS.CONTENT)
.values("jooq test", "content of Jooq test")
.returningResult(POSTS.ID)
val postId = Mono.from(insertPostSql).awaitSingle()
log.debug(" postId: $postId")
val insertCommentSql = dslContext.insertInto(COMMENTS)
.columns(COMMENTS.POST_ID, COMMENTS.CONTENT)
.values(postId.component1(), "test comments")
.values(postId.component1(), "test comments 2")
val insertedCount = Mono.from(insertCommentSql).awaitSingle()
log.info(" insertedCount: $insertedCount")
val querySQL = dslContext
.select(
POSTS.TITLE,
POSTS.CONTENT,
multiset(
select(COMMENTS.CONTENT)
.from(COMMENTS)
.where(COMMENTS.POST_ID.eq(POSTS.ID))
).`as`("comments")
)
.from(POSTS)
.orderBy(POSTS.CREATED_AT)
Flux.from(querySQL).asFlow()
.onEach { log.info("querySQL result: $it") }
.collect()
val posts = postRepository.findByKeyword("test").toList()
posts shouldNotBe null
posts.size shouldBe 1
posts[0].commentsCount shouldBe 2
postRepository.countByKeyword("test") shouldBe 1
}
// other tests
My example project is based Postgres, R2dbc, And Spring Data R2dbc: https://github.com/hantsy/spring-r2dbc-sample/blob/master/jooq-kotlin-co-gradle
Related
I have an application.yml with some configuration properties required by my application.
SF:
baseurl: https://xxxxx
case:
recordTypeId: 0124a0000004Ifb
application:
recordTypeId: 0125P000000MkDa
address:
personal:
recordTypeId: 0125P000000MnuO
business:
recordTypeId: 0125P000000MnuT
I have defined a configuration class to read those properties as follows:
#Configuration
class SFProperties(
#Value("\${sf.case.recordTypeId}") val caseRecordTypeId: String,
#Value("\${sf.application.recordTypeId}") val applicationRecordTypeId: String,
#Value("\${sf.address.personal.recordTypeId}") val addressPersonalRecordTypeId:String,
#Value("\${sf.address.business.recordTypeId}") val addressBusinessRecordTypeId: String
)
The class is wired within a service without any issues,
#Service
class SFClientManagementServiceImpl( val webClientBuilder: WebClient.Builder):
ClientManagementService {
....
#Autowired
lateinit var sfProperties: SFProperties
override fun createCase(caseRequest: CaseRequestDto): Mono<CaseResponseDto> {
...
var myValue= sfProperties.caseRecordTypeId
....
}
}
When trying to test this service, I get a "lateinit property sfProperties has not been initialized" exception:
The test looks as follows:
#SpringBootTest(classes = [SFProperties::class])
class SalesforceClientManagementServiceImplTests {
#Autowired
open lateinit var sfProperties: SFProperties
#Test
fun `createCase should return case id when case is created`() {
val clientResponse: ClientResponse = ClientResponse
.create(HttpStatus.OK)
.header("Content-Type", "application/json")
.body(ObjectMapper().writeValueAsString(Fakes().GetFakeCaseResponseDto())).build()
val shortCircuitingExchangeFunction = ExchangeFunction {
Mono.just(clientResponse)
}
val webClientBuilder: WebClient.Builder = WebClient.builder().exchangeFunction(shortCircuitingExchangeFunction)
val sfClientManagementServiceImpl =
SFClientManagementServiceImpl(webClientBuilder)
var caseResponseDto =
salesforceClientManagementServiceImpl.createCase(Fakes().GetFakeCaseRequestDto())
var response = caseResponseDto.block()
if (response != null) {
assertEquals(Fakes().GetFakeCaseResponseDto().id, response.id)
}
}
I have tried many other annotations on the Test class but without success, I would appreciate any ideas.
After reading the docs of #Tailable of Spring Data MongoDB, I think it is good to use it for message notifications.
#SpringBootApplication
class ServerApplication {
#Bean
fun runner(template: ReactiveMongoTemplate) = CommandLineRunner {
println("running CommandLineRunner...")
template.executeCommand("{\"convertToCapped\": \"messages\", size: 100000}");
}
fun main(args: Array<String>) {
runApplication<ServerApplication>(*args)
}
}
---------
#RestController()
#RequestMapping(value = ["messages"])
#CrossOrigin(origins = ["http://localhost:4200"])
class MessageController(private val messages: MessageRepository) {
#PostMapping
fun hello(p: String) =
this.messages.save(Message(body = p, sentAt = Instant.now())).log().then()
#GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun messageStream(): Flux<Message> = this.messages.getMessagesBy().log()
}
-----------
interface MessageRepository : ReactiveMongoRepository<Message, String> {
#Tailable
fun getMessagesBy(): Flux<Message>
}
------------
#Document(collection = "messages")
data class Message(#Id var id: String? = null, var body: String, var sentAt: Instant = Instant.now())
How to implement it?
Done it by myself, check my solution.
I have resolved this issue myself, check the sample codes.
Also, published a post on medium to demonstrate how to use it a SPA client written in Angular.
What is need
I'm writing an application (Spring + Kotlin) that takes information with Kafka. If I set autoStartup = "true" when declaring a #KafkaListener then the app works fine but only if broker is available. When the broker is unavailable application crashes on start. It's undesirable behavior. The application must work and perform other functions.
What I tried to do
For the escape of crashing application on start somebody on this site in another topic advised setting autoStartup = "false" when declaring a #KafkaListener. And it really helped to prevent crash on start. But now I cannot successfully start KafkaListener manually. In other examples I saw auto wiring of KafkaListenerEndpointRegistry, but when I trying to do it:
#Service
class KafkaConsumer #Autowired constructor(
private val kafkaListenerEndpointRegistry: KafkaListenerEndpointRegistry
) {
IntelliJ Idea warns:
Could not autowire. No beans of 'KafkaListenerEndpointRegistry' type found.
When I try to use KafkaListenerEndpointRegistry without autowiring and perform this code:
#Service
class KafkaConsumer {
private val logger = LoggerFactory.getLogger(this::class.java)
private val kafkaListenerEndpointRegistry = KafkaListenerEndpointRegistry()
#Scheduled(fixedDelay = 10000)
fun startCpguListener(){
val container = kafkaListenerEndpointRegistry.getListenerContainer("consumer1")
if (!container.isRunning)
try {
logger.info("Kafka Consumer is not running. Trying to start...")
container.start()
} catch (e: Exception){
logger.error(e.message)
}
}
#KafkaListener(
id = "consumer1",
topics = ["cpgdb.public.user"],
autoStartup = "false"
)
private fun listen(it: ConsumerRecord<JsonNode, JsonNode>, qwe: Consumer<Any, Any>){
val pay = it.value().get("payload")
val after = pay.get("after")
val id = after["id"].asInt()
val receivedUser = CpguUser(
id = id,
name = after["name"].asText()
)
logger.info("received user with id = $id")
}
}
}
kafkaListenerEndpointRegistry.getListenerContainer("consumer1") always return null. I guess it's because I didn't auto wire kafkaListenerEndpointRegistry. How can I do it? Or if exist another solution of my answer I'll be appreciative any help! Thanks!
There is Kafka config:
#Configuration
#EnableConfigurationProperties(KafkaProperties::class)
class KafkaConfiguration(private val props: KafkaProperties) {
#Bean
fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<Any, Any> {
val factory = ConcurrentKafkaListenerContainerFactory<Any, Any>()
factory.consumerFactory = consumerFactory()
factory.setConcurrency(1)
factory.setMessageConverter(MessagingMessageConverter())
factory.setStatefulRetry(true)
val retryTemplate = RetryTemplate()
retryTemplate.setRetryPolicy(AlwaysRetryPolicy())
retryTemplate.setBackOffPolicy(ExponentialBackOffPolicy())
factory.setRetryTemplate(retryTemplate)
val handler = SeekToCurrentErrorHandler()
handler.isAckAfterHandle = false
factory.setErrorHandler(handler)
factory.containerProperties.isMissingTopicsFatal = false
return factory
}
#Bean
fun consumerFactory(): ConsumerFactory<Any, Any> {
return DefaultKafkaConsumerFactory(consumerConfigs())
}
#Bean
fun consumerConfigs(): Map<String, Any> {
return mapOf(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to props.bootstrap.address,
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java,
ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG to listOf(MonitoringConsumerInterceptor::class.java),
ConsumerConfig.CLIENT_ID_CONFIG to props.receiver.clientId,
ConsumerConfig.GROUP_ID_CONFIG to props.receiver.groupId,
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest",
ConsumerConfig.ISOLATION_LEVEL_CONFIG to "read_committed",
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG to true
)
}
}
spring boot version: 2.3.0
spring-kafka version: 2.5.3
kafka-clients version: 2.5.0
Just ignore IntelliJ's warning about the auto wiring; the bean does exist; it's just that IntelliJ can't detect it.
I am playing around with Kofu functional Bean DSL. I am using Spring-Data-JDBC with Spring-MVC and trying to autowire NamedParameterJdbcTemplate. However, I am have been receiving this error that no beans found for it while running tests. In a annotation based approach, we don’t have to supply an explicit NamedParameterJdbcTemplate. My sample app here: https://github.com/overfullstack/kofu-mvc-jdbc. And PFB some code snippets from it:
val app = application(WebApplicationType.SERVLET) {
beans {
bean<SampleService>()
bean<UserHandler>()
}
enable(dataConfig)
enable(webConfig)
}
val dataConfig = configuration {
beans {
bean<UserRepository>()
}
listener<ApplicationReadyEvent> {
ref<UserRepository>().init()
}
}
val webConfig = configuration {
webMvc {
port = if (profiles.contains("test")) 8181 else 8080
router {
val handler = ref<UserHandler>()
GET("/", handler::hello)
GET("/api", handler::json)
}
converters {
string()
jackson()
}
}
}
class UserRepository(private val client: NamedParameterJdbcTemplate) {
fun count() =
client.queryForObject("SELECT COUNT(*) FROM users", emptyMap<String, String>(), Int::class.java)
}
open class UserRepositoryTests {
private val dataApp = application(WebApplicationType.NONE) {
enable(dataConfig)
}
private lateinit var context: ConfigurableApplicationContext
#BeforeAll
fun beforeAll() {
context = dataApp.run(profiles = "test")
}
#Test
fun count() {
val repository = context.getBean<UserRepository>()
assertEquals(3, repository.count())
}
#AfterAll
fun afterAll() {
context.close()
}
}
This is the error:
Parameter 0 of constructor in com.sample.UserRepository required a bean of type 'org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate' that could not be found.
Action:
Consider defining a bean of type 'org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate' in your configuration.
Please help, thanks
Apparently Kofu doesn't pick datasource from application.properties file. Everything is meant to be declarative and no implicit derivations. (Basically no Spring magic 🙂). This worked for me:
val dataConfig = configuration {
beans {
bean {
val dataSourceBuilder = DataSourceBuilder.create()
dataSourceBuilder.driverClassName(“org.h2.Driver”)
dataSourceBuilder.url(“jdbc:h2:mem:test”)
dataSourceBuilder.username(“SA”)
dataSourceBuilder.password(“”)
dataSourceBuilder.build()
}
bean<NamedParameterJdbcTemplate>()
bean<UserRepository>()
}
listener<ApplicationReadyEvent> {
ref<UserRepository>().init()
}
}
Written a short convenicence extension for Testcontainers:
fun JdbcDatabaseContainer<*>.execute(query:DSLContext.()-> Query){
val connection = DriverManager.getConnection(this.getJdbcUrl(),this.getUsername(),this.getPassword())
val create = DSL.using(connection)
create.query().execute()
}
And now wanted to test it.
Flyway loads 30 entries. These should be visible in allDataPresent
canInsert inserts one entry without the extension
canInsertWithExtension does the same but via the extension function
insertMultipleWithExtension does exactly as its name implies and inserts another 5
All but the allDataPresent testcase (because that one is read-only anyway) are annotated #Transactional.
As such, I'd expect these modifications to be rolled back after the test method.
What instead happens is
[ERROR] Failures:
[ERROR] InitDataIT.allDataPresent:70
Expecting:
<36>
to be equal to:
<30>
but was not.
[ERROR] InitDataIT.canInsert:90
Expecting:
<6>
to be equal to:
<1>
but was not.
[ERROR] InitDataIT.canInsertWithExtension:112
Expecting:
<6>
to be equal to:
<1>
but was not.
Each #Test is working fine on its own. So the issue must lie with the #Transactional.
So why is that? And more importantly, how do I get the rollbacks?
Full testcase (also tried annotating the class instead, didn't make any difference):
#Testcontainers
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ContextConfiguration(initializers = [InitDataIT.TestContextInitializer::class])
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
open class InitDataIT {
companion object {
#JvmStatic
#Container
private val dbContainer = MySQLContainer<Nothing>().apply {
withDatabaseName("test")
withUsername("root")
withPassword("")
}
}
object TestContextInitializer: ApplicationContextInitializer<ConfigurableApplicationContext> {
override fun initialize(applicationContext: ConfigurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=${dbContainer.jdbcUrl}",
"spring.datasource.username=${dbContainer.username}",
"spring.datasource.password=${dbContainer.password}",
"spring.datasource.driver-class-name=${dbContainer.driverClassName}"
).applyTo(applicationContext)
}
}
private val create:DSLContext
#Autowired
constructor(create:DSLContext){
this.create = create
}
#Test
fun allDataPresent(){
//given
val expectedNumberOfEntries = 30
val query = create.selectCount()
.from(CUSTOMERS)
//when
val numberOfEntries = query.fetchOne{it.value1()}
//then
Assertions.assertThat(numberOfEntries).isEqualTo(expectedNumberOfEntries)
}
#Test
#Transactional
open fun canInsert(){
//given
val insertquery = create.insertInto(CUSTOMERS)
.columns(CUSTOMERS.FIRSTNAME,CUSTOMERS.LASTNAME,CUSTOMERS.EMAIL, CUSTOMERS.STATUS)
.values("Alice","Tester","Alice.Tester#somewhere.tt",CustomerStatus.Contacted.name)
val expectedNumberInOffice2 = 1
//when
insertquery.execute()
//then
val numberInOffice2 = create.selectCount()
.from(CUSTOMERS)
.where(CUSTOMERS.EMAIL.contains("somewhere"))
.fetchOne{it.value1()}
assertThat(numberInOffice2).isEqualTo(expectedNumberInOffice2)
}
#Test
#Transactional
open fun canInsertWithExtension(){
//given
dbContainer.execute {
insertInto(CUSTOMERS)
.columns(CUSTOMERS.FIRSTNAME,CUSTOMERS.LASTNAME,CUSTOMERS.EMAIL, CUSTOMERS.STATUS)
.values("Alice","Tester","Alice.Tester#somewhere.tt",CustomerStatus.Contacted.name)
}
val expectedNumberInOffice2 = 1
//when
val numberInOffice2 = create.selectCount()
.from(CUSTOMERS)
.where(CUSTOMERS.EMAIL.contains("somewhere"))
.fetchOne{it.value1()}
//then
assertThat(numberInOffice2).isEqualTo(expectedNumberInOffice2)
}
#Test
#Transactional
open fun insertMultipleWithExtension(){
//given
dbContainer.execute {
insertInto(CUSTOMERS)
.columns(CUSTOMERS.FIRSTNAME,CUSTOMERS.LASTNAME,CUSTOMERS.EMAIL, CUSTOMERS.STATUS)
.values("Alice","Make","Alice.Make#somewhere.tt", CustomerStatus.Customer.name)
.values("Bob","Another","Bob.Another#somewhere.tt", CustomerStatus.ClosedLost.name)
.values("Charlie","Integration","Charlie.Integration#somewhere.tt",CustomerStatus.NotContacted.name)
.values("Denise","Test","Denise.Test#somewhere.tt",CustomerStatus.Customer.name)
.values("Ellie","Now","Ellie.Now#somewhere.tt",CustomerStatus.Contacted.name)
}
val expectedNumberInOffice2 = 5
//when
val numberInOffice2 = create.selectCount()
.from(CUSTOMERS)
.where(CUSTOMERS.EMAIL.contains("somewhere"))
.fetchOne{it.value1()}
//then
assertThat(numberInOffice2).isEqualTo(expectedNumberInOffice2)
}
}
The Spring #Transactional annotation doesn't just magically work with your DriverManager created JDBC connections. Your dbContainer object should operate on your spring managed data source instead.