Blog Infos
Author
Published
Topics
, , , ,
Published
Introduction

When you’re building an Android app — or even a cross-platform Kotlin Multiplatform (KMM) project — that relies on network calls, things can get slow and unreliable if you’re always calling a real server — especially during testing.

By combining MockKKoin, and Ktor, you can “fake” your network responses, so your tests and development stay smooth and reliable — even when offline.

Note: Although we use Ktor in this article (because it’s Kotlin-friendly and perfect for KMM), the MockK + Koin approach easily applies to any networking library — such as OkHttpRetrofit, or others. As long as you wrap your network calls in an interface (like ApiService), you can swap in a mock or a real implementation as needed.

Enter MockK

MockK is a Kotlin-first mocking library that offers:

  • Native Kotlin support: Ideal for projects using coroutines or advanced Kotlin features.
  • Clear syntaxevery { ... } returns ... to define how mocks respond, and verify/coVerify to confirm calls.
  • Flexible options: Like “relaxed” mocks to reduce boilerplate, and coVerify for suspend functions.

Because MockK is pure Kotlin, it often feels more natural than Java-oriented libraries — especially on modern Android and KMM projects.

Why Ktor?

Ktor is a Kotlin-native framework for building and consuming HTTP APIs. It works on the JVM, Android, iOS (via Kotlin/Native), and more. In Android apps, you might use it as your HTTP client, but in KMM projects, you can share this network logic across multiple platforms:

  1. Pure Kotlin: Suits KMM perfectly — no platform-specific bridging for networking.
  2. Lightweight & Composable: Configure pipeline features like logging, JSON parsing, and timeouts.
  3. Multiplatform: Same code for Android, iOS, desktop, or server-based projects.

When using MockK to fake Ktor responses, you effectively short-circuit real network requests, returning data as if Ktor had made the call — without hitting an actual server. This synergy is what makes your tests fast, reliable, and easy to maintain in both Android and KMM environments.

Mockito vs MockK

Historically, Mockito has been the go-to solution for Java-based mocking, and it can work in Kotlin with some additional steps (like enabling inline mocking or adding Kotlin extensions). However, MockK:

  • Natively supports Kotlin: Suspending functions, extension methods, etc.
  • Offers “relaxed” mocks to cut down on boilerplate.
  • Tends to require fewer workarounds, making it particularly appealing for Kotlin Multiplatform code, where Java-specific assumptions might break.
Use Case: Faking Ktor Responses

Imagine your app (or KMM module) fetches user profiles, weather data, or anything else from a remote API via Ktor. If you rely on a real server in your tests or staging environment, you might face:

  • Slower tests (network latency).
  • Flaky results (server downtime or changing data).
  • Difficult offline testing (losing network disrupts your tests).

Instead, you can:

  1. Use the real Ktor service in production.
  2. Switch to a mock for tests (or “stage” environment) returning predictable data, no internet required.

This is where MockK (for mocking), Koin (for easy dependency injection), and Ktor (for flexible HTTP clients) align perfectly. A simple toggle, like useMocks = true, decides whether to rely on real or fake Ktor calls.

Implementation Steps
1. Environment & Mock Settings

First, create a small data class to hold environment info:

data class EnvironmentConfig(
    val environmentName: String,  // e.g. "production", "stage"
    val useMocks: Boolean
)

You can define these values in many ways — via BuildConfig, code constants, or even your CI pipeline. Below are two approaches to wire them up in Koin:

(A) Defining Environment Info in a Koin Module

 

val envModule = module {
    single {
        EnvironmentConfig(
            environmentName = "stage",
            useMocks = true
        )
    }
}

 

Here, you simply declare the environment name ("stage") and set useMocks = true so any module that depends on EnvironmentConfig can get it from Koin.

(B) Using BuildConfig to Define Environment Info

 

// In your app-level build.gradle
android {
    defaultConfig {
        // Example build config fields for environment + mock usage
        buildConfigField "String", "ENVIRONMENT", "\"stage\""
        buildConfigField "boolean", "USE_MOCKS", "true"
    }
}

 

Then in Kotlin code:

// You can still define a data class, or just read from BuildConfig directly:
val environmentName = BuildConfig.ENVIRONMENT
val useMocks = BuildConfig.USE_MOCKS

This approach is handy if you want to automatically tie environment flags to different build variants (e.g., debugrelease, or custom “staging” variants).

2. Real API Service (Ktor)

Then you’ll need an interface plus a real implementation using Ktor:

// Important to use interface
interface ApiService {
    suspend fun fetchData(): String
}

// This implementation utilizes the interface to return real data
class RealApiService : ApiService {
    private val client = io.ktor.client.HttpClient() {
        // e.g. logging, timeouts, JSON config
    }

    override suspend fun fetchData(): String {
        // Calls a real endpoint
        return client.get("https://example.com/data")
    }
}

If useMocks = false, you’d provide RealApiService in your code or DI setup. For KMM, this same code could be shared across Android and iOS modules—Ktor works on both.

3. Creating a Mock with MockK

Faking the Ktor calls in MockK is straightforward:

import io.mockk.every
import io.mockk.mockk

val mockService = mockk<ApiService>()

every { mockService.fetchData() } returns "Fake response data from MockK!"
  • mockk() creates a mock object.
  • every { … } returns … tells it to return a certain value whenever fetchData() is called.

You can also use answers which gives you a lambda, thus a place to add extra code (rather than just returning a value) if you want something dynamic, like a timestamp.

every { mockService.fetchData() } answers {
    "Response with timestamp: ${System.currentTimeMillis()}"
}

If you don’t specify how a method behaves, MockK returns defaults (null, 0, false, etc.) for that method.

4. Injecting Dependencies with Koin

Use Koin to switch between mock and real services, depending on your environment.

Below is an example that uses the Koin module approach for environment info. If you prefer BuildConfig, just replace the lines where we retrieve EnvironmentConfig with direct calls to BuildConfig.ENVIRONMENT and BuildConfig.USE_MOCKS.

val networkModule = module {
    single<ApiService> {
        val config: EnvironmentConfig = get()  // from envModule if using approach (A)

        // If you prefer BuildConfig approach, do something like:
        // val environmentName = BuildConfig.ENVIRONMENT
        // val useMocks = BuildConfig.USE_MOCKS

        if (config.environmentName == "stage" && config.useMocks) {
            // Provide a mock
            val mockService = mockk<ApiService>()
            every { mockService.fetchData() } returns "Fake response data from MockK!"
            mockService
        } else {
            RealApiService()
        }
    }
}

// Also you can have a module to hold environment info if not in BuildConfig
// In approach (A) we saw this environment module
val envModule = module {
    single {
        EnvironmentConfig(
            environmentName = "stage",
            useMocks = true
        )
    }
}
  • If environmentName = "stage" and useMocks = true, you inject the mock ApiService.
  • Otherwise, you get RealApiService.
Why Koin?
  • Lightweight DI framework, well-suited for Kotlin (and KMM if you share modules).
  • Simplifies the “if-else” logic for providing mocks vs. real objects.
  • Makes it easy to override modules in tests, staging, or production builds.
Overriding Modules in Tests

If you prefer different modules for production vs. testing, you can override them in your test setup using loadKoinModules() or the override = true flag.

Testing with runTest

Modern Android apps often rely on coroutines. Add the coroutines test library to your build.gradle:

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:YOUR_VERSION_HERE"

Then wrap your test logic in runTest:

@Test
fun testFetchDataWithMockK() = runTest {
    startKoin { modules(listOf(envModule, networkModule)) }

    // Because environment = "stage" + useMocks = true, you get a mock
    val service: ApiService = get()

    val data = service.fetchData()
    assertEquals("Fake response data from MockK!", data)
}

This ensures no real network calls happen and that your tests are fast and consistent.

Testing the ViewModel

Below is a quick sample of how to test a ViewModel that depends on ApiService:

// The ViewModel
class MyViewModel(private val apiService: ApiService) : ViewModel() {
    val data = MutableLiveData<String>()

    fun loadData() {
        viewModelScope.launch {
            data.value = apiService.fetchData()
        }
    }
}

// Our Test class
class MyViewModelTest {

    @Before
    fun setupKoin() {
        // Start Koin once per test class (or per test if you prefer)
        startKoin {
            modules(listOf(envModule, networkModule))
        }
    }

    @After
    fun tearDownKoin() {
        // Stop Koin to avoid polluting other tests
        stopKoin()
    }

    @Test
    fun `ViewModel uses mocked service with verify`() = runTest {
        // The mock or real service is injected based on environment settings
        val viewModel = MyViewModel(get())

        viewModel.loadData()

        // Our mock returns "Fake response data from MockK!"
        assertEquals("Fake response data from MockK!", viewModel.data.value)

        // Check that fetchData() was indeed called once using verify
        val service: ApiService = get()
        io.mockk.verify(exactly = 1) { service.fetchData() }
    }
}
Notes:
  1. @Before and @After allow you to set up and tear down Koin cleanly.
  2. You can replicate this logic in a base test class or a JUnit test rule/extension if you have many test classes.
verify vs. coVerify in MockK

In MockK, both verify and coVerify can work with suspending functions, but:

1. coVerify

coVerify is explicitly designed for suspending (coroutine) calls.

  • It ensures the verification is aligned with coroutine context handling, which can help avoid subtle concurrency or timing issues.
  • It tells readers of your test, “I’m verifying a coroutine call,” improving clarity and maintainability.

2. verify

verify can pass most of the time if you’re testing suspending code inside runTest, but in complex or asynchronous flows (e.g., parallel coroutines, multiple suspending calls, time manipulation with advanceTimeBy), coVerify might handle completion states more reliably.

When to use coVerify
  • Whenever you’re verifying multiple or complex suspending calls: If your test triggers concurrency or sequential calls that can’t be guaranteed to finish instantly, coVerify reduces the risk of timing issues.
  • When clarity is key: Marking a verification as coVerify signals you’re intentionally dealing with coroutine-based calls, which helps other developers.
  • If you run into race conditions using verify in a more complex coroutine scenario, switching to coVerify often resolves them.

If your scenario is super simple — one call, straightforward flow, all in runTestverify and coVerify will likely behave identically. In that case, verify is perfectly fine. But coVerify gives you a safer, more explicit guarantee in real-world coroutine usage.

coVerify Example: Multiple Suspending Calls

Imagine a scenario where your ViewModel calls two suspending methods on the same service, possibly in parallel. With verify, the test might pass or fail depending on timing, especially if the second call hasn’t completed when verify runs. With coVerify, MockK tries to ensure the coroutine calls have completed before verifying.

Service With Two Suspensions

 

interface ApiService {
    suspend fun fetchUsers(): List<String>
    suspend fun fetchPosts(): List<String>
}

class RealApiService : ApiService {
    override suspend fun fetchUsers(): List<String> {
        // Some real network call
    }
    override suspend fun fetchPosts(): List<String> {
        // Another real network call
    }
}

 

ViewModel Triggering Both

 

class MyViewModel(private val apiService: ApiService) : ViewModel() {
    val users = MutableLiveData<List<String>>()
    val posts = MutableLiveData<List<String>>()

    fun loadAllData() {
        viewModelScope.launch {
            // In real life, you might do these in parallel or with concurrency structures
            users.value = apiService.fetchUsers()
            posts.value = apiService.fetchPosts()
        }
    }
}

 

Test Using coVerify

 

class MyViewModelTest {

    @Test
    fun `test loadAllData with coVerify`() = runTest {
        // Suppose we inject a mockk<ApiService> via Koin or manual injection
        val mockService = mockk<ApiService>()
        every { mockService.fetchUsers() } returns listOf("Alice", "Bob")
        every { mockService.fetchPosts() } returns listOf("Post1", "Post2")

        val viewModel = MyViewModel(mockService)
        viewModel.loadAllData()  // calls both suspending methods

        // Check results
        assertEquals(listOf("Alice", "Bob"), viewModel.users.value)
        assertEquals(listOf("Post1", "Post2"), viewModel.posts.value)

        // coVerify ensures all coroutines completed for these calls before verifying
        coVerify(exactly = 1) { mockService.fetchUsers() }
        coVerify(exactly = 1) { mockService.fetchPosts() }
    }
}

 

In some advanced tests (e.g., if you used runTest { ... } but had parallel coroutines plus time-shifting with advanceTimeBy or advanceUntilIdle), verify might incorrectly run before the second call finishes. coVerify is better at ensuring it waits for suspend calls to complete (or at least that it knows they’re coroutines) before checking if they were called.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Intro to unit testing coroutines with Kotest & MockK

In this workshop, you’ll learn how to test coroutines effectively using the Kotest and MockK libraries, ensuring your app handles concurrent tasks efficiently and with confidence.
Watch Video

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michali ...
Kotlin GDE

Jobs

No results found.

KMM Considerations

Because KtorKoin, and MockK are all pure Kotlin, you can use them in Kotlin Multiplatform Mobile (KMM) projects as well:

  • Ktor client code can be shared across Android and iOS.
  • MockK can test your shared (common) Kotlin code that includes business logic and network layers.
  • Koin can be used to inject these dependencies in a KMM environment (though you might handle DI differently on iOS if you want a more native approach).

Regardless, the same principles apply: mock your ApiService so you’re not hitting a real server on each test run, saving time and complexity across both platforms.

Extra Use Cases for MockK

Besides faking HTTP requests, MockK can also simplify other use cases. Here are just a few ideas:

1. Database Testing

Quickly test your repository logic without a real DB.

val mockDao = mockk<UserDao>()
every { mockDao.getUsers() } returns listOf(User("Alice"))
2. Analytics & Logging

Confirm logs are called correctly — no real analytics server needed.

val mockAnalytics = mockk<AnalyticsService>()
// ...
io.mockk.verify { mockAnalytics.trackEvent("ScreenViewed") }
3. Push Notifications

The inverse = true parameter (or verify(exactly = 0)) ensures sendNotification wasn’t called.

val mockPushManager = mockk<PushManager>()
io.mockk.verify(inverse = true) { mockPushManager.sendNotification(any()) }

. . .

Wrapping Up

By mixing MockK with Ktor and Koin, you can:

  • Short-circuit real network calls in your tests, boosting speed and reliability.
  • Toggle between a mock or real service using either a Koin module or BuildConfig flags.
  • Test coroutines seamlessly, thanks to MockK’s Kotlin-native design.

Mockito is still a solid library, but MockK usually feels more natural for modern Kotlin — especially if you heavily use suspending functions or advanced Kotlin features. Whichever approach you pick, mocking your network layer (and other dependencies) is a surefire way to keep your tests stable, your code modular, and your development process friction-free. Enjoy your streamlined testing workflow!

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu