I'm working on a Kotlin Ktor project and encountering an issue with mocking dependencies in my endpoint tests using Kotest, Koin, and Mockk. Specifically, the mocked use cases in my tests are not being utilized as expected within the userRoutes
function.
Here's a snippet of my test setup where I mock various use cases:
class UserRoutesTest : FunSpec() {
// Mocking use cases
private var getUserOffersUseCase: GetUserOffersUseCase = mockk()
private var getUserInfoUseCase: GetUserInfoUseCase = mockk()
private var userWishlistUseCase: UserWishlistUseCase = mockk()
private var saveUserUseCase: SaveUserUseCase = mockk()
private var updateUserDataUseCase: UpdateUserDataUseCase = mockk()
private val mockUserDtoService = MockUserDtoService()
private val fakeUser = mockUserDtoService.randomBasicUserDto()
init {
coEvery { getUserInfoUseCase.getBasicInfo(fakeUser.id) } returns fakeUser
test("GET /{userId} should return BasicUserDto") {
testApplication {
application {
routing {
userRoutes(getUserOffersUseCase, getUserInfoUseCase, userWishlistUseCase, saveUserUseCase, updateUserDataUseCase)
}
}
val response = client.get("/user/${fakeUser.id}")
response.status shouldBe HttpStatusCode.OK
}
}
// ...
}
}
When i debug the test i see that the reference to getUserInfoUseCase
in the test environment (for example GetUserInfoUseCase@92335
) differs from the reference within the userRoutes
function (GetUserInfoUseCase@717fd87c
). This leads to the test failing as it attempts to access the real database instead of using the mocked data.
Has anyone encountered a similar issue or can provide insight into why the mocked instances aren't being used as expected in the userRoutes
function?
I have tried to inject references with Koin
private val testModule = module {
single { getUserOffersUseCase }
single { getUserInfoUseCase }
single { userWishlistUseCase }
single { saveUserUseCase }
single { updateUserDataUseCase }
init{
startKoin {
modules(testModule)
}
}
userRoutes
fun Route.userRoutes(
getUserOffersUseCase: GetUserOffersUseCase,
getUserInfoUseCase: GetUserInfoUseCase,
userWishlistUseCase: UserWishlistUseCase,
saveUserUseCase: SaveUserUseCase,
updateUserDataUseCase: UpdateUserDataUseCase
) {
route("/user") {
get("/{userId}") {
val userId = call.parameters["userId"] ?: throw MissingRequestParameterException("userId")
val user: BasicUserDto = getUserInfoUseCase.getBasicInfo(userId)
call.respondSuccess(data = user)
}
same issue with Junit 5 tests:
class UserRoutesTest: KoinTest {
private val getUserInfoUseCase: GetUserInfoUseCase by inject()
val fakeUser = MockUserDtoService().randomBasicUserDto()
val userId = fakeUser.id
val userTestModule = module {
// Mocking the UserDataRepository
single<UserDataRepository> { mockk(relaxed = true) }
// Mocking each use case with MockK
single { mockk<GetUserOffersUseCase>(relaxed = true) }
single { mockk<GetUserInfoUseCase>(relaxed = true) }
single { mockk<UserWishlistUseCase>(relaxed = true) }
single { mockk<SaveUserUseCase>(relaxed = true) }
single { mockk<UpdateUserDataUseCase>(relaxed = true) }
}
@BeforeEach
fun setUp() {
stopKoin()
startKoin {
modules(userTestModule)
}
coEvery { getUserInfoUseCase.getBasicInfo(any()) } returns fakeUser
}
@AfterEach
fun tearDown() {
stopKoin()
}
@Test
fun `GET user by userId returns user info`() = testApplication {
application {
configureRouting()
}
this.client.get("/user/$userId").apply {
assertEquals(HttpStatusCode.OK, status)
val responseBody = bodyAsText()
assertNotNull(responseBody)
}
}
private fun Application.configureRouting() {
this.routing {
userRoutes(getUserInfoUseCase = getUserInfoUseCase)
}
}
UPDATE
Using userTestModule doesn't make a difference, it works just like injecting dependencies manually... No matter what I do, userRoute() always ends up with a different object than what I put in the test. It keeps looking for the user in the real database. For now this is how my routes looks like:
fun Application.configureRouting() {
routing {
val createOfferUseCase: CreateOfferUseCase by inject()
val getAllOffersUseCase: GetOffersUseCase by inject()
val getOfferByIdUseCase: GetOfferByIdUseCase by inject()
val updateOfferUseCase: UpdateOfferUseCase by inject()
offerRoutes(
createOfferUseCase,
getAllOffersUseCase,
getOfferByIdUseCase,
updateOfferUseCase
)
categoryRoutes()
userRoutes()
get("/hello_world"){
call.respondSuccess("Hello world", HttpStatusCode.OK)
}
}
}
fun Route.userRoutes(
getUserOffersUseCase: GetUserOffersUseCase = get(),
getUserInfoUseCase: GetUserInfoUseCase = get(),
userWishlistUseCase: UserWishlistUseCase = get(),
saveUserUseCase: SaveUserUseCase = get(),
updateUserDataUseCase: UpdateUserDataUseCase = get()
) {
route("/user") {
get("/{userId}") {
val userId = call.parameters["userId"] ?: throw MissingRequestParameterException("userId")
val user: BasicUserDto = getUserInfoUseCase.getBasicInfo(userId)
call.respondSuccess(data = user)
}
//...
fun main(args: Array<String>) {
io.ktor.server.netty.EngineMain.main(args)
}
fun Application.module() {
initKoin()
configureSecurity()
configureSerialization()
configureStatusPages()
configureRouting()
}
fun Application.initKoin() {
install(Koin) {
slf4jLogger()
modules(listOf(appModule, databaseModule, categoryModule, offerModule, userModule, authModule))
}
}
val userModule = module {
single<UserDataRepository> { UserDataRepositoryImpl(get(named("UserCollection"))) }
single { GetUserOffersUseCase(get()) }
single { GetUserInfoUseCase(get()) }
single { UserWishlistUseCase(get()) }
single { SaveUserUseCase(get()) }
single { UpdateUserDataUseCase(get()) }
}
This is the solution.
Thanks to Aleksei Tirman, who gave me a hint on the Kotlin Slack to add this code line.
Working test