Blog Infos
Author
Published
Topics
, , , ,
Author
Published

When building Android applications, writing unit tests is crucial to ensure your code behaves as expected. As your app grows in complexity, testing individual components becomes more challenging, especially when there are dependencies between different parts of your code. This is where mocks and spies come into play, helping you isolate and test units effectively without interference from dependencies.

In this article, we’ll explore the roles of mocks and spies in Android unit testing using Kotlin. We’ll also go over examples to help you apply these concepts in your Android projects.

What is Unit testing?

Unit testing is a type of software testing where individual units or components of a software application are tested in isolation from the rest of the system. A unit refers to the smallest testable part of the application, such as a function, method, or class. The primary goal of unit testing is to ensure that each unit of the software performs as expected, independent of other parts of the system.

Key Characteristics of Unit Tests:
  1. Isolated Testing: Unit tests focus on testing a single piece of functionality in isolation. This means that dependencies (like databases, APIs, or other classes) are often mocked or stubbed out to prevent side effects or dependencies from influencing the test.
  2. Fast and Lightweight: Since unit tests don’t rely on external systems, they are typically fast and lightweight. They don’t involve setting up databases, network connections, or other system components.
  3. Automated: Unit tests are usually automated and executed frequently (e.g., as part of a continuous integration (CI) pipeline). This allows for quick feedback during development.
  4. Repeatable: Unit tests should be deterministic, meaning they produce the same result every time they run, assuming the code under test hasn’t changed.
Benefits of Unit Testing:
  1. Bug Detection: Unit tests help detect issues early in the development cycle, catching bugs before they make their way into production code.
  2. Improved Code Quality: Writing unit tests encourages developers to write more modular and cleaner code. Since unit tests work best on small, self-contained functions or classes, this often leads to better-designed code.
  3. Refactoring with Confidence: Unit tests serve as a safety net when refactoring code. Since unit tests verify the functionality of each component, developers can refactor code confidently, knowing that existing functionality is still preserved.
  4. Documentation: Unit tests can act as living documentation. Well-written tests describe the expected behavior of functions or classes, making it easier for new developers to understand how a particular piece of code is supposed to work.
  5. Faster Debugging: Unit tests make debugging easier by quickly narrowing down which specific part of the code is malfunctioning.
What Are Mocks and Spies?

Before diving into the code, let’s first understand what mocks and spies are.

  • Mock: A mock is a fake object that mimics the behavior of a real object. When writing unit tests, mocks allow us to replace dependencies with controlled behavior to test the code in isolation.
// Start mock
List mockedList = mock(List.class);
// Using mock object
mockedList.add("one");
mockedList.clear();
// Verificatiion
verify(mockedList).add("one");
verify(mockedList).clear();
  • Spy: A spy wraps around an actual object. It lets you keep the object’s real behavior but allows you to “spy” on method calls, check interactions, or override specific behaviors.

You can create spies of real objects. When you use the spy then the real methods are called (unless a method was stubbed). Real spies should be used carefully and occasionally, for example when dealing with legacy code.

List list = new LinkedList();
List spy = spy(list);
// Optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
 
// Using the spy calls real methods
spy.add("one");
spy.add("two");
 
// Prints "one" - the first element of a list
System.out.println(spy.get(0));
 
// Size() method was stubbed - 100 is printed
System.out.println(spy.size());
 
// Optionally, you can verify
verify(spy).add("one");
verify(spy).add("two");
Why Use Mocks and Spies?

Mocks and spies play an essential role in isolating the unit of code being tested, especially when that unit depends on external systems (like databases, APIs, or other services). By replacing real dependencies with mocks or spies, we can:

  • Test the logic of a class without relying on real dependencies.
  • Avoid external side effects like network calls or database reads/writes.
  • Control the behavior of external systems during testing.

This leads to faster and more reliable tests since there’s no need to set up or access external systems.

When to Use Mocks vs Spies?
  • Mocks: Use when you want to completely replace a dependency and don’t care about its real implementation. Mocks are typically used for dependencies such as APIs, databases, or services that interact with external systems.
  • Spies: Use when you want to keep the real behavior of a class but monitor its methods or partially modify some of its behavior. Spies are useful when you want to test a real class but need to track its interactions.
Differences Between Mocks and Spies

 

Mock Spy
Replaces a real object entirely Uses the real object but monitors it
Doesn't call real methods by default Calls real methods unless specified.
Primarily used for behavior definition Primarily used for behavior observation.
view raw mock+spy.md hosted with ❤ by GitHub
Practical Example
Let’s take a look about this diagram above, we have 2 class:

 

Imaging we have a GetAppConfigUseCase class that fetches data from a GetAppConfigRepository, we don’t want to hit a real repository or database, so we’ll mock GetAppConfigRepository to control its behavior.

GetAppConfigRepository
// Domain
class AppConfig(val name: String)
// Data
internal class AppConfigResponse(
val id: String,
val name: String
)
internal class AppConfigRepositoryImpl : AppConfigRepository {
override suspend fun getConfig(
onSuccess: (AppConfig) -> Unit, onFailure: (Throwable?) -> Unit
) {
withContext(Dispatchers.IO) {
try {
val id = UUID.randomUUID().toString()
val response = AppConfigResponse(id, id.substring(0, 10))
onSuccess(AppConfig(name = response.name))
} catch (e: Exception) {
onFailure(e)
}
}
}
}
view raw mock+spy.kt hosted with ❤ by GitHub
GetAppConfigUseCase
class GetAppConfigUseCase(private val repository: AppConfigRepository) {
suspend operator fun invoke() {
repository.getConfig(
onSuccess = ::save,
onFailure = ::logError
)
}
fun save(config: AppConfig) {
}
fun logError(throwable: Throwable?) {
}
}
view raw mock+spy.kt hosted with ❤ by GitHub
GetAppConfigUseCaseUnitTest

Now let’s write the unit tests using Mockito to mock the GetAppConfigUseCase class, and to verify that the onSuccess and onFailure methods are called correctly. Create a test class in your src/test/java/ folder and write the unit test using mock like below:

@RunWith(MockitoJUnitRunner::class)
class GetAppConfigUseCaseTest {
private lateinit var testObject: GetAppConfigUseCase
@Mock
private lateinit var repository: AppConfigRepository
@Before
fun setUp() {
testObject = GetAppConfigUseCase(repository)
}
@Test
fun `GIVEN AppConfig WHEN call invoke THEN see correct AppConfig`() = runTest {
// Given
// Use spy to verify internal method calls
testObject = spy(testObject)
val expectedAppConfig = AppConfig("name")
// Capture the onSuccess and onFailure callbacks
val onSuccess = argumentCaptor<(AppConfig) -> Unit>()
val onFailure = argumentCaptor<(Throwable?) -> Unit>()
// When
testObject.invoke()
// Verify that onSuccess, onFailure were called and capture the callbacks
verify(repository).getConfig(onSuccess.capture(), onFailure.capture())
// Simulate onSuccess callback being triggered
onSuccess.firstValue.invoke(expectedAppConfig)
// Then
// Verify onSuccess is called
verify(testObject).save(expectedAppConfig)
}
}
view raw mock+spy.kt hosted with ❤ by GitHub

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

  • Mock: create a mock for GetAppConfigRepository using @Mock
  • Argument Capture: We use argumentCaptor to capture the callbacks passed to getConfig so we can invoke them manually in the test.
  • Spies: spy(testObject) allows us to verify the internal method calls (onSuccess and onFailure) while preserving the logic of the UseCase class.

Test Cases:

  • Success Case: The test checks if save is called when onSuccess is triggered.
  • Failure Case: The test checks if logError is called with the correct parameters when onFailure is triggered.

Required Dependencies

// Unit testing libraries
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:3.11.2'
testImplementation 'org.mockito:mockito-inline:3.11.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:1.5.21"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.5.21"
Conclusion

Unit testing is a crucial part of Android development, ensuring that individual components work as expected without relying on external systems. By using mocks and spies, you can isolate your code, control dependencies, and write fast, reliable tests.

  • Mocks replace real objects with fakes to control their behavior.
  • Spies wrap around real objects to monitor or modify their behavior during tests.

With this knowledge, you can now confidently implement unit tests in your Android projects using mocks and spies in Kotlin!

Besides mock and spy, there are several other techniques, tools, and practices that support and enhance unit testing in Android development with Kotlin. These help in writing more efficient, maintainable, and reliable tests. Here’s an overview of additional concepts and tools that support unit testing:

1. Stubs
  • What is it? A stub is a simplified implementation of a class or method that returns hard-coded data. Unlike mocks, stubs do not verify interactions or behavior; they are just placeholders that provide predefined responses.
  • When to use: Use stubs when you need a predictable and simple result without focusing on how it’s called.
class FakeUserRepository : UserRepository {
    override fun getUserById(userId: String): User {
        // Returning a hard-coded user for testing
        return User("1", "Jane Doe")
    }
}

Stubs are useful when you don’t need the flexibility of mocks, but just want simple data for your test cases.

2. Fake Objects
  • What is it? A fake is a simple, usually in-memory implementation of an interface or class, which mimics the behavior of the real object. Fakes are used when creating the real object is too expensive (e.g., database connections, network calls).
  • When to use: Use fakes to simulate interactions with external systems, like databases or APIs, but with simplified behavior.
class InMemoryUserRepository : UserRepository {
    private val users = mutableListOf<User>()

    override fun getUserById(userId: String): User {
        return users.find { it.id == userId } ?: throw NoSuchElementException("User not found")
    }

    fun addUser(user: User) {
        users.add(user)
    }
}

Here, InMemoryUserRepository is a fake that mimics the real repository but uses an in-memory list to simulate database functionality.

3. Test Fixtures
  • What is it? A fixture is a known set of data or objects that is used to ensure tests run with consistent inputs. Test fixtures make sure that your test runs in a controlled environment by setting up the necessary preconditions.
  • When to use: Use fixtures when you need a specific state or set of data before running your tests, ensuring consistency across different tests.
class UserServiceTest {

    private lateinit var userService: UserService
    private lateinit var mockRepository: UserRepository

    @Before
    fun setup() {
        mockRepository = mock(UserRepository::class.java)
        `when`(mockRepository.getUserById("1")).thenReturn(User("1", "John Doe"))
        userService = UserService(mockRepository)
    }

    @Test
    fun `should return user name correctly`() {
        val userName = userService.getUserName("1")
        assertEquals("John Doe", userName)
    }
}

In this case, the @Before annotated method setup() acts as a fixture by initializing and configuring the necessary objects before each test.

4. Assertions
  • What is it? Assertions are used to check the correctness of the test’s results. They are critical in verifying that the expected outcomes match the actual results. Frameworks like JUnit provide built-in assertions, while libraries like Hamcrest and Truth offer more expressive and readable assertions.
  • When to use: Use assertions in every test to verify that the actual output matches the expected behavior.

Example using JUnit Assertions:

@Test
fun `should return user name correctly`() {
    val userName = userService.getUserName("1")
    assertEquals("John Doe", userName)
}

Example using Hamcrest:

import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat

@Test
fun `should return user name correctly`() {
    val userName = userService.getUserName("1")
    assertThat(userName, `is`("John Doe"))
}

Hamcrest’s assertThat() is more readable and is especially useful for complex assertions.

5. Parameterized Tests
  • What is it? A parameterized test allows you to run the same test with different sets of data. This is useful when you want to test multiple cases without duplicating test logic.
  • When to use: Use parameterized tests when you need to verify a method or class with multiple inputs and outputs.
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.Assert.assertEquals

@RunWith(Parameterized::class)
class CalculatorTest(private val input1: Int, private val input2: Int, private val expected: Int) {

    companion object {
        @JvmStatic
        @Parameterized.Parameters
        fun data() = listOf(
            arrayOf(1, 2, 3),
            arrayOf(2, 3, 5),
            arrayOf(5, 5, 10)
        )
    }

    private val calculator = Calculator()

    @Test
    fun `test addition`() {
        assertEquals(expected, calculator.add(input1, input2))
    }
}

This allows you to test the Calculator.add() method with various inputs without writing multiple test methods.

6. Test Doubles (Dummies)
  • What is it? A test double is a general term for any object used in place of a real one in unit tests. Mocks, stubs, and fakes are all forms of test doubles. Dummies are a type of test double that is passed around but never actually used. They are only there to satisfy parameter requirements.
  • When to use: Use dummies when you need to provide an argument but don’t care about its behavior during the test.
class OrderServiceTest {
    @Test
    fun `should process order`() {
        val dummyCustomer = Customer(id = "123")  // Not used, just to satisfy the signature
        val orderService = OrderService(dummyCustomer)
        orderService.processOrder()
    }
}
7. Timeouts
  • What is it? A timeout specifies the maximum duration that a test is allowed to run before being forcibly terminated. This ensures that your tests do not hang indefinitely.
  • When to use: Use timeouts to avoid long-running tests, especially when dealing with asynchronous code or network operations.
@Test(timeout = 1000)  // Test will fail if it runs longer than 1 second
fun `test method with timeout`() {
    Thread.sleep(500)  // This will pass
}
8. Code Coverage Tools
  • What is it? Code coverage tools help you measure how much of your code is exercised by your unit tests. Tools like JaCoCo can show you what percentage of your code is covered by tests and highlight areas that are not being tested.
  • When to use: Use code coverage tools to identify untested parts of your codebase and improve your test suite.
// Add the following to your build.gradle to enable JaCoCo:

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.8.7"
}

tasks.jacocoTestReport {
    reports {
        xml.enabled = true
        html.enabled = true
    }
}

// Run the following to generate a coverage report:
./gradlew test jacocoTestReport
9. Test Runners
  • What is it? A test runner is a component that orchestrates the execution of your test cases. In Android, the default test runner is AndroidJUnitRunner, but you can also use custom runners like Robolectric for simulating Android components in unit tests.
  • When to use: Use a test runner to execute and manage your tests, particularly when you have special requirements like testing UI or Android components.
10. Robolectric
  • What is it? Robolectric is a framework that allows you to run Android tests on the JVM without the need for an emulator or real device. It provides shadow implementations of Android APIs.
  • When to use: Use Robolectric to test Android components (like ActivityFragmentView) in isolation without requiring a device or emulator.
@RunWith(RobolectricTestRunner::class)
class MainActivityTest {

    @Test
    fun `should display correct text`() {
        val activity = Robolectric.buildActivity(MainActivity::class.java).create().get()
        val textView = activity.findViewById<TextView>(R.id.text_view)
        assertEquals("Hello World!", textView.text.toString())
    }
}
References:

https://github.com/nphausg?source=post_page—–66e23b0e330b——————————–

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE
Menu