startKoin in KoinTest-class throws "A KoinContext is already started" - kotlin

I'm using "withTestAppliction" in one of my tests to test if the route works. Before all Tests the DB-Table "cats" should have no entries. To get the DAO I need Koin in this Test but if conflicts with "withTestAppliction" where Koin will also be startet and throws A KoinContext is already started
[Update]
I know I could use something like handleRequest(HttpMethod.Delete, "/cats") but I don't want to expose this Rest-Interface. Not even for testing.
#ExperimentalCoroutinesApi
class CatsTest: KoinTest {
companion object {
#BeforeClass
#JvmStatic fun setup() {
// once per run
startKoin {
modules(appModule)
}
}
#AfterClass
#JvmStatic fun teardown() {
// clean up after this class, leave nothing dirty behind
stopKoin()
}
}
#Before
fun setupTest() = runBlockingTest {
val dao = inject<CatDAO>()
dao.value.deleteAll()
}
#After
fun cleanUp() {
}
#Test
fun testCreateCat() {
withTestApplication({ module(testing = true) }) {
val call = createCat(predictName("Pepples"), 22)
call.response.status().`should be`(HttpStatusCode.Created)
}
}
}
fun TestApplicationEngine.createCat(name: String, age: Int): TestApplicationCall {
return handleRequest(HttpMethod.Post, "/cats") {
addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(listOf(
"name" to name,
"age" to age.toString()
).formUrlEncode())
}
}

After test (after withTestApplication()) call KoinContextHandler.get().stopKoin().
Example: https://github.com/comm1x/ktor-boot/blob/master/test/common/common.kt

It looks similar to the issue I faced. The problem was that the module() passed under the withTestApplication() was trying to create the Koin object again. I replaced the module() with specific modules that I had to load for the tests except for the Koin.
Refer - test sample and
application sample

Had the same problem executing multiple tests in a class. After removing the init/startKoin (since it's initialized in the Application when you test with the emulator).
I am not really sure if this is the correct approach, but it kind of works for me and my build server.
#ExperimentalCoroutinesApi
#RunWith(AndroidJUnit4ClassRunner::class) // or JUnit4..
class MyTest : KoinTest {
private val mockedAppModule: Module = module(override = true)
factory { myRepo }
}
#Before
fun setup() {
loadKoinModules(mockedAppModule)
}
#After
fun tearDown() {
unloadKoinModules(mockedAppModule)
}
#Test
fun testSubscriberRegistration() = runBlockingTest { // only needed if you are using supend functions
// test impl...
}
}

Related

Mockk #OverrideMockKs not working with Kotest

I've using Kotest recently and I hadn't had any issues, but recently I was trying some annotations for dependency injection so to simplify the problem I created some basic classes with some methods that just print some messages, just for the sake of learning how to use Kotest and Mockk, but during the testing, I ran with the exception that the variable hasn't been initialized when trying to run the test.
These are my classes
class DefaultClass : AbstractClass() {
private val anotherClass: AnotherClass = AnotherClass()
fun testMethod(value: String): String {
val normalizeValue = value.trim().lowercase().replace(Regex("[^ A-Za-z\\d]*"), "")
return runBlocking {
anotherClass.someOtherMethod()
callsProtectedMethod(normalizeValue)
}
}
private suspend fun callsProtectedMethod(value: String) = coroutineScope {
println("Original method")
returnDefaultString(value)
}
}
AnotherClass
class AnotherClass {
fun someOtherMethod(): Unit {
println("SomeOtherMethod original")
}
}
Test
class DefaultClassTest : FunSpec({
context("Testing DefaultClass") {
#MockK
lateinit var anotherClass: AnotherClass
#OverrideMockKs
lateinit var defaultClass: DefaultClass
beforeContainer {
MockKAnnotations.init(this)
}
test("testing mocks") {
defaultClass.testMethod("some method")
}
}
I've changed the initialization to beforeTest, taken it out of the context, and also use beforeContainer, beforeTest, beforeSpec, but none of these work... every time I still get lateinit property defaultClass has not been initialized
So, I recreated the same test using JUnit and I don't have this issue.
class DefaultClassJUnitTest {
companion object {
#MockK
lateinit var anotherClass: AnotherClass
#OverrideMockKs
lateinit var defaultClass: DefaultClass
#BeforeAll
#JvmStatic
fun setup() {
MockKAnnotations.init(this)
}
}
#Test
fun `Testing with JUnit`() {
every { anotherClass.someOtherMethod() } answers {
println("Mocking another class")
}
val value = defaultClass.testMethod("some method")
}
}
So I'm pretty sure that I'm doing something wrong when using Kotest. I hope anyone might help me, thanks...
I think MockK is probably not looking for variables defined within function scopes. If you want to use the annotations, you likely have to move them to the companion object, like this:
class DefaultClassTest : FunSpec({
context("Testing DefaultClass") {
beforeContainer {
MockKAnnotations.init(this)
}
test("testing mocks") {
defaultClass.testMethod("some method")
}
}
}) {
companion object {
#MockK
lateinit var anotherClass: AnotherClass
#OverrideMockKs
lateinit var defaultClass: DefaultClass
}
}

Kotlin flows junit test freezes when to test collect method

I'm trying to write a component which uses different datasources of data.
Then data is combined and emitted in the different resulting flow.
class TaskControlComponent(
private val diskCacheDataSource: DiskCacheDataSource,
private val debugDataSource: DebugDataSource
) {
private val _localTasks = MutableStateFlow<Map<String, TaskItem>>(emptyMap())
val localTasks: StateFlow<Map<String, TaskItem>> = _localTasks
suspend fun loadLocal() {
flowOf(
diskCacheDataSource.defaultFeatures,
diskCacheDataSource.localFeatures,
debugDataSource.debugFeatures
).flattenMerge().collect {
computeLocalTasks()
}
}
private suspend fun computeLocalTasks() {
val resultTasks = HashMap<String, TaskItem>(64)
listOf(
diskCacheDataSource.defaultTasks,
diskCacheDataSource.localTasks,
debugDataSource.debugTasks
).forEach { tasksMap ->
tasksMap.value.forEach { entry ->
resultTasks[entry.key] = entry.value
}
}
_localTasks.emit(resultTasks)
}
}
DataSource
interface DiskCacheDataSource {
val defaultTasks: StateFlow<Map<String, TaskItem>>
val localTasks: StateFlow<Map<String, TaskItem>>
}
It works, but how to write junit test for that?
class TaskControlImplTest {
private lateinit var taskControl: TaskControlComponent
#Mock
lateinit var diskCacheDataSource: DiskCacheDataSource
#Mock
lateinit var debugDataSource: DebugDataSource
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
taskControl = TaskControlComponent(diskCacheDataSource, debugDataSource)
}
#Test
fun testFeatureControl() {
whenever(diskCacheDataSource.defaultTasks).thenReturn(
MutableStateFlow(
mapOf(
"1" to TaskItem(
"1",
TaskStatus.On
)
)
)
)
whenever(diskCacheDataSource.localTasks).thenReturn(MutableStateFlow(emptyMap()))
whenever(debugDataSource.debugTasks).thenReturn(MutableStateFlow(emptyMap()))
runBlocking {
taskControl.loadLocal()
}
runBlocking {
taskControl.localTasks.collect {
Assert.assertEquals(it.size, 1)
}
}
}
}
In case of the following sequence of commands
runBlocking {
taskControl.loadLocal()
}
runBlocking {
taskControl.localTasks.collect {
Assert.assertEquals(it.size, 1)
}
}
test freezes, and runs forewer.
When I swap the pieces of code, first instead of second and the contrary
runBlocking {
featureControl.localFeatures.collect {
Assert.assertEquals(it.size, 1)
}
}
runBlocking {
featureControl.loadLocal()
}
Tests finishes with warning
expected:<0> but was:<1>
Expected :0
Actual :1
Is it possible to write test for such usecase? What should be investigated or done in order to make test workable?
The reason the order matters here is because StateFlow is hot, unlike normal Flow, meaning it starts running with data immediately not when it is collected
I test with the turbine library but you don't need it. I don't remember the exact setup of not using turbine but it was a bit more complicated so I chose to use turbine
https://github.com/cashapp/turbine

Dagger2 Use a Provider<T> for #BindsInstance

Is it possible to bind a Provider<T> instead of just an instance T?
For example, I have this:
#Component()
interface Module {
#Component.Builder
interface Builder {
#BindsInstance
fun config(config: Config): Builder
}
}
I would like to do something like this (but it doesn't work):
#Component()
interface Module {
#Component.Builder
interface Builder {
#BindsInstance
fun config(config: Provider<Config>): Builder
}
}
What am I trying to do?
NOTE: The Config class is immutable
I would like to be able to change the config value for different tests, e.g.
class Test {
val configProvider: CustomProvider<Config>
val classUnderTest: Provider<T>
#Before
fun setUp() {
val module = DaggerModule()
.setConfig(configProvider)
.build()
// Do other setup with module
classUnderTest = module.getTestClassProvider()
}
#Test
fun test() {
configProvider.setValue(CONFIG_1)
classUnderTest.get().doSomething();
}
#Test
fun test2() {
configProvider.setValue(CONFIG_2)
classUnderTest.get().doSomething();
}
}
I'm trying to avoid having to finish building the module in each test case and then calling another function to handle finishing the setup, e.g.
#Before
fun setUp() {
moduleBuilder = DaggerModule()
}
#Test
fun test() {
val module = moduleBuilder.setConfig(CONFIG_1).build()
finishSetup(module)
// Do test
}
Thanks for your help!

MockK - cannot mock same function twice

I am trying to test the getTopicNames function (below) in two scenarios: If it succeeds and if it does not succeed.
fun getTopicNames(): Either<Exception, Set<String>> =
try {
adminClient.listTopics()
.names()
.get()
.right()
} catch (exception: ExecutionException) {
exception.left()
}
This is the test class in which I am doing those two scenarios. If I run each test individually, they both suceed. If I run the entire class the second to execute fails because for some reason the previous mock on adminClient.listTopics() is being retained.
These are the versions for everything involved:
kotlin: 1.3.72
koin: 2.1.6
junit: 5.6.1
mockk: 1.10.0
class TopicOperationsTest {
#BeforeEach
fun start() {
val testModule = module(createdAtStart = true) {
single { mockk<AdminClient>() }
}
startKoin { modules(testModule) }
}
#AfterEach
fun stop() {
stopKoin()
}
#Test
fun `getTopicNames() returns a Right with the topics names`() {
val adminClient = get(AdminClient::class.java)
val listOfTopicsToReturn = mockk<ListTopicsResult>()
val expectedTopics = setOf("Topic1", "Topic2", "Topic3")
every { adminClient.listTopics() } returns listOfTopicsToReturn
every { listOfTopicsToReturn.names() } returns KafkaFuture.completedFuture(expectedTopics)
println("listOfTopicsToReturn.names(): " + listOfTopicsToReturn.names())
println("adminClient.listTopics(): " + adminClient.listTopics())
println("getTopicNames(): " + getTopicNames())
assertThat(getTopicNames().getOrElse { emptySet() }, `is`(expectedTopics))
}
#Test
fun `getTopicNames() returns a Left if failing to get topic names`() {
val adminClient = get(AdminClient::class.java)
every { adminClient.listTopics() } throws ExecutionException("Some Failure", Exception())
assertThat(getTopicNames(), IsInstanceOf(Either.Left::class.java))
}
}
This is the error I get, caused by the fact that the test that verifies the failure is the first to run:
java.lang.AssertionError:
Expected: is <[Topic1, Topic2, Topic3]>
but: was <[]>
Expected :is <[Topic1, Topic2, Topic3]>
Actual :<[]>
<Click to see difference>
Already tried clearAllMocks() on the BeforeEach method but it does not solve my problem as I just start getting:
io.mockk.MockKException: no answer found for: AdminClient(#1).listTopics()
I found a solution that makes everything work. It is a combination of:
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
Having the mock as a class object
MockKAnnotations.init(this) in the #BeforeEach method
clearMocks() specifying the actual mock to be cleared (should work for multiple mocks too, just separated by commas.
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TopicOperationsTest {
private var adminClientMock = mockk<AdminClient>()
#BeforeEach
fun start() {
MockKAnnotations.init(this)
val testModule = module(createdAtStart = true) {
single { adminClientMock }
}
startKoin { modules(testModule) }
}
#AfterEach
fun stop() {
clearMocks(adminClientMock)
stopKoin()
}
#Test
fun `getTopicNames() returns a Right with the topics names`() {
val adminClient = get(AdminClient::class.java)
val listOfTopicsToReturn = mockk<ListTopicsResult>()
val expectedTopics = setOf("Topic1", "Topic2", "Topic3")
every { adminClient.listTopics() } returns listOfTopicsToReturn
every { listOfTopicsToReturn.names() } returns KafkaFuture.completedFuture(expectedTopics)
assertThat(getTopicNames().getOrElse { emptySet() }, `is`(expectedTopics))
}
#Test
fun `getTopicNames() returns a Left if failing to get topic names`() {
val adminClient = get(AdminClient::class.java)
every { adminClient.listTopics() } throws ExecutionException("Some Failure", Exception())
assertThat(getTopicNames(), IsInstanceOf(Either.Left::class.java))
}
}

Mockk: Verify method called within coroutine

I have a simple object which provides a suspend function to simulate a delaying network request and afterwards calls another method from the object.
class CoroutinesObject {
suspend fun doApiCall() {
delay(1000)
println("Hello from API")
val apiResult = "result #1"
callMe(apiResult)
}
fun callMe(result: String) {
println("[${Thread.currentThread().name}] call me with result: $result")
}
}
I would like to write a simple test which should verify that the method callMe has been called.
class CoroutinesTest {
#Test
fun doApiCall_callsCallMe() {
val obj = CoroutinesObject()
runBlocking {
obj.doApiCall()
}
coVerify { obj.callMe("result #1") }
}
}
Unfortunately the test fails with the following exception and I'm not sure why this happens.
io.mockk.MockKException: Missing calls inside verify { ... } block.
Anybody got an idea whats the problem and how to write a test which is able to verify the called method?
Okay, it seems as if a missing mock for my object was the problem. The following test works:
#Test
fun doApiCall_callsCallMe() {
val obj = spyk(CoroutinesObject())
runBlocking {
obj.doApiCall()
}
coVerify { obj.callMe(any()) }
}