0

Note: I'm a Kotlin beginner

Developing stack

  • Ktor
  • Koin
  • KMongo: MongoDB

I defined my application to be like this

Route -> Service -> Repository
  1. Route is define all HTTP request endpoints.
  2. Service is business logic of application.
  3. Repository is data access layer for query/persist data to MongoDB.

What I did to test a route, if the simple route like.

fun Route.healthCheck() {
    get("/health") {
        call.respond(HealthCheckResponse(message = "OK"))
    }
}

@Test
fun testHealth() = testApplication {
    application {
        configureRouting()
    }
    client.get("/health").apply {
        expect {
            that(status).isEqualTo(HttpStatusCode.OK)
            that(contentType()?.contentType).isNotNull().and {
                contains(ContentType.Application.Json.contentType)
            }
            that(Json.decodeFromString(HealthCheckResponse.serializer(), bodyAsText()).message)
                .isEqualTo("OK")
        }
    }
}

Test above will run good.

But when the case of the route that has DI, injecting CoroutineDatabase object into Repository then that repository inject it to Service and the service inject into Route.

In unit test code, I defined like below.

// Route define
fun Application.configureRouting() {
    routing {
        user()
    }
}
fun Route.user() {
    val userService: UserService by inject()
    ...
}
class UserServiceImpl(
    private val userRepository: UserRepository // <- Repository is injected with MongoDB `CoroutineDatabase` object.
) : AccountService {
    ...
}
===============
// Unit Test
class UserEndpointTest: KoinTest {
@get:Rule
val koinTestRule = KoinTestRule.create {
    modules(module{ single { accountService } })
}
@Test
fun testUserEndpoint() = testApplication {
    application {
        configureRouting() // -> collecting all extended function of `Route`
    }
    val client = createClient {
        install(ContentNegotiation) {
            json()
        }
    }
    val testerEmail = "i_am_testing@the-email.dev"
    val requestJson = """
    {
        "email": "$testerEmail",
        "password": "aBCdEf9"
    }
    """.trimIndent()
    val testBody = jsonMapper.decodeFromString(CreateAccountRequest.serializer(), requestJson)

    coEvery { mockAccountService.submitUser(any()) } returns User(id = newId(), email = testerEmail, password = "")

    client.post("$accountEndpoint/user") {
        contentType(ContentType.Application.Json)
        setBody(testBody)
    }.apply {
        expect {
            that(status).isEqualTo(HttpStatusCode.Created)
            that(contentType()?.contentType).isNotNull().and {
                contains(ContentType.Application.Json.contentType)
            }
            that((body() as AccountResponse).id).isNotBlank()
            that((body() as AccountResponse).email).isNotBlank().and {
                isEqualTo(testerEmail)
            }
        }
    }
}}

I expected I did mocking service and it should be inject into Route then it won't chaining call to repository and CoroutineDatabase object.

But when I run it keep connecting to database, I noticed with log below.

2022-09-07 02:51:30.093 [cluster-ClusterId{value='631788a0b273ac122eaa8350', description='null'}-localhost:27017] DEBUG org.mongodb.driver.cluster - Updating cluster description to  {type=UNKNOWN, servers=[{address=localhost:27017, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused}}]

And I tried to find out a tutorial to find how to write Unit Test with scenario like these, but I found most of answers on searched results are before Ktor version up. (Noticed from their codes still using withTestApplication {} which is deprecated already in version 2.1.0

Who can explain the step of Koin DI work and how to interrupt it with mock.

Thanks.

Natta Wang
  • 553
  • 11
  • 18

0 Answers0