Hey! I’m struggling a bit making Kotest, Koin and ...
# koin
d
Hey! I’m struggling a bit making Kotest, Koin and Ktor work together. Specifically overriding a
single
defined in the main application with a
Mockk
in tests. I was expecting Koin to pickup the
customerServiceMock
instead of the production
CustomerService
, but it doesn’t. Here’s a sample of the test:
Copy code
class APITest :
    FunSpec(),
    KoinTest {

    override fun extensions(): List<Extension> = listOf(
        WireMockListener(jwksWireMockServer, ListenerMode.PER_SPEC),
        KoinExtension(
            module = module {},
            mockProvider = { mockkClass(it, relaxed = true) },
            mode = KoinLifecycleMode.Root,
        ),
    )

    init {
        val testDb = install(DatabaseTestConfig.jdbcTestExtension).createTestDatabase()

        val appConfig =
            listOf(
                ApplicationConfig("application.test.conf"),
                testDb.applicationConfig,
                JwksWireMockSetup.appConfig,
            ).reduce(ApplicationConfig::mergeWith)

        test("Empty tables and no defaults should return empty results") {
            testApplication {
                environment {
                    config = appConfig
                }

                val customerServiceMock = declareMock<CustomerService>()

                coEvery { customerServiceMock.getCustomer(any()) }.returns(Customer(CustomerId("1"), 30))

                val cli = createTestClient()

                cli.get("/api/v1/recommendations?customerId=1").apply {
                    status shouldBe HttpStatusCode.OK
                    body<Recommendations>() shouldBe Recommendations.empty()
                }
            }
        }
    }
}
@arnaud.giuliani I have a reproduction sample here: https://github.com/mityakoval/KoinKotestSample
Not sure if it’s a bug or a feature, but I noticed that the issue arises if
routing
is configured in complicated-ish way.
Copy code
import com.example.routing.testRouting
import io.ktor.server.application.Application

fun Application.configureRouting() {
    testRouting()
}

...

import com.example.services.ServiceA
import com.example.services.ServiceB
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import kotlinx.serialization.Serializable
import org.koin.ktor.ext.inject

fun Application.testRouting() {
    routing {
        val serviceA by inject<ServiceA>()
        val serviceB by inject<ServiceB>()
        test(serviceA, serviceB)
    }
}

@Serializable
data class Response(val a: String, val b: String)


fun Route.test(serviceA: ServiceA, serviceB: ServiceB) {
    get("/test") {
        call.respond(status = HttpStatusCode.OK, Response(serviceA.identify(), serviceB.identify()))
    }
}
If I just stuff all routes in
routing {}
in
configureRouting()
the mocks are injected correctly.
a
yeah, I'vn't tried this kind of test with kotest. Perhaps @LeoColman?
👀 1
l
I believe the problem lies in Ktor's `testApplication`:
Copy code
Note: If you have the application. conf file in the resources folder, testApplication loads all modules and properties specified in the configuration file automatically.
Your
application.conf
is defined as
Copy code
ktor {
  deployment {
    port = 8080
  }
  application {
    modules = [com.example.ApplicationKt.module]
  }
}
and
com.example.ApplicationKt.module
ends up calling
Copy code
fun Application.configureKoin() = install(Koin) {
    slf4jLogger()

    val config = module {
        single<ServiceA> { ServiceA() }
        single<ServiceB> { ServiceB() }
    }

    modules(config)
}
So by the time your test starts, Koin is already ready to go with an actual instance of
ServiceB
and it's no longer possible to replace it with another implementation by using the regular
declareMock
. My guess for your work around is that you skipped the
testApplication
bit that sets Koin up. I'm not an ace in Ktor, so I might have missed a point