Introduction and Context Setting

Welcome to the second lesson in our exploration of Test-Driven Development (TDD) using Kotlin, JUnit, and Mockito. In the previous lesson, we covered how to utilize dummies to isolate dependencies. This lesson will focus on learning about another type of test double — Stubs.

By the end of this lesson, you will understand what stubs are and how to implement them in your tests, specifically for isolating dependencies like external services within the Kotlin ecosystem.

Understanding Stubs in Testing

Stubs provide predefined answers to method calls during testing. Unlike other test doubles, stubs do not track their usage, making them simpler yet powerful for certain scenarios.

Stubs are particularly useful when testing functions that rely on external services or complex dependencies. By simulating function outputs, stubs make tests faster and more predictable. Keep in mind that stubs focus on ensuring your application's logic functions as expected without verifying the correctness of external dependencies.

In Kotlin, Mockito is a popular library used to create stubs. It allows us to set up return values that simulate how dependencies should behave in a controlled environment. This predictability isolates and tests your application's logic without relying on the behavior of external systems, which might be complex or introduce variability.

Example: Crafting a `WeatherAlertService` Using Stubs

We will build a WeatherAlertService that fetches data from a WeatherService. This service will issue alerts based on specific conditions. The external data source is impractical for testing, so we'll use stubbed data for our tests instead.

Red: Writing the First Test

Create a new test class named WeatherAlertServiceTest.kt with the following test setup:

Kotlin
1import org.junit.jupiter.api.BeforeEach 2import org.junit.jupiter.api.Test 3import kotlin.test.assertEquals 4 5class WeatherAlertServiceTest { 6 7 private lateinit var weatherService: WeatherServiceStub 8 private lateinit var alertService: WeatherAlertService 9 10 class WeatherServiceStub : IWeatherService { 11 private var currentWeather: WeatherData = WeatherData(20, "sunny") 12 13 fun setWeather(temperature: Int, conditions: String) { 14 this.currentWeather = WeatherData(temperature, conditions) 15 } 16 17 override fun getCurrentWeather(location: String): WeatherData { 18 return currentWeather 19 } 20 } 21 22 @BeforeEach 23 fun setUp() { 24 weatherService = WeatherServiceStub() 25 alertService = WeatherAlertService(weatherService) 26 } 27 28 @Test 29 fun shouldReturnHeatWarningWhenTemperatureIsAbove35() { 30 // Arrange 31 weatherService.setWeather(36, "sunny") 32 33 // Act 34 val alert = alertService.shouldSendAlert("London") 35 36 // Assert 37 assertEquals("Extreme heat warning. Stay hydrated!", alert) 38 } 39}

In this test:

  • We create a hand-crafted stub WeatherServiceStub that implements the IWeatherService interface. This stub allows us to customize the weather data by setting predefined values for temperature and conditions via the setWeather method.
  • The stub’s getCurrentWeather method returns a WeatherData object with these predefined values, simulating expected weather conditions for the test scenario without using Mockito.
  • The JUnit test validates whether the WeatherAlertService correctly interprets the weather data and returns the appropriate alert, specifically checking if it generates a heat warning when the temperature exceeds 35 degrees.

Run this test, and expect it to fail initially, as WeatherAlertService is not yet implemented to handle the specified conditions.

Green: Making the Test Pass

Implement the WeatherAlertService class with minimal logic:

Kotlin
1class WeatherAlertService(private val weatherService: IWeatherService) { 2 3 fun shouldSendAlert(location: String): String? { 4 val weather = weatherService.getCurrentWeather(location) 5 6 return if (weather.temperature > 35) { 7 "Extreme heat warning. Stay hydrated!" 8 } else { 9 null 10 } 11 } 12}

Also, implement the supporting interfaces and classes:

Kotlin
1interface IWeatherService { 2 fun getCurrentWeather(location: String): WeatherData 3}
Kotlin
1data class WeatherData(val temperature: Int, val conditions: String)

Run the tests again. The objective is to pass the specific test scenario by implementing only the necessary logic without overengineering other cases.

Refactor: Introduce a Stub Using the Mockito Library

In the initial implementation, we created a manual stub, WeatherServiceStub, to simulate weather data. Now, we will refactor this by leveraging the Mockito library to streamline the process and improve maintainability. Using Mockito, we can create flexible and reusable stubs with minimal code.

The Mockito library allows us to set up method expectations and return values dynamically. By substituting our hand-crafted stub with a Mockito-based one, we achieve the same test outcomes but with enhanced clarity and simplicity. This approach decreases the potential for bugs and makes adjusting test parameters easier.

Here's how we refactor our WeatherAlertServiceTest:

  • Replace the WeatherServiceStub with a mock(IWeatherService::class.java).
  • Use the Mockito.when and thenReturn methods in Mockito to define the expected inputs and outputs for the getCurrentWeather method.
  • Through Mockito, we eliminate the need to manage state explicitly within our custom stub class.

The refactored test class using the Mockito library is shown below:

Kotlin
1import org.junit.jupiter.api.BeforeEach 2import org.junit.jupiter.api.Test 3import org.mockito.Mockito 4import kotlin.test.assertEquals 5 6class WeatherAlertServiceTest { 7 8 private lateinit var weatherServiceMock: IWeatherService 9 private lateinit var alertService: WeatherAlertService 10 11 @BeforeEach 12 fun setUp() { 13 weatherServiceMock = Mockito.mock(IWeatherService::class.java) 14 alertService = WeatherAlertService(weatherServiceMock) 15 } 16 17 @Test 18 fun shouldReturnHeatWarningWhenTemperatureIsAbove35() { 19 // Arrange 20 Mockito.`when`(weatherServiceMock.getCurrentWeather("London")) 21 .thenReturn(WeatherData(36, "sunny")) 22 23 // Act 24 val alert = alertService.shouldSendAlert("London") 25 26 // Assert 27 assertEquals("Extreme heat warning. Stay hydrated!", alert) 28 } 29}

This transition toward using Mockito simplifies test management and focuses the tests more on behavior rather than setup intricacies.

Using Mockito Annotations for Simplified Setup (JUnit 5)

Alternatively, you can use annotations like @Mock and @InjectMocks along with @ExtendWith(MockitoExtension::class) annotation to further simplify setup and initialization:

Kotlin
1import org.junit.jupiter.api.extension.ExtendWith 2import org.mockito.InjectMocks 3import org.mockito.Mock 4import org.mockito.junit.jupiter.MockitoExtension 5 6@ExtendWith(MockitoExtension::class) 7class WeatherAlertServiceTest { 8 9 @Mock 10 lateinit var weatherServiceMock: IWeatherService 11 12 @InjectMocks 13 lateinit var alertService: WeatherAlertService 14 15 // Test methods... 16}

The @ExtendWith(MockitoExtension::class) annotation activates Mockito annotations like @Mock and @InjectMocks. The @Mock annotation creates a mock instance of IWeatherService, and @InjectMocks automatically injects this mock into the WeatherAlertService.

This approach eliminates the need for manual mock initialization and makes the tests cleaner and more maintainable. It also reduces boilerplate code, allowing you to focus more on the behavior being tested.

Summary and Preparation for Practice

In this lesson, we delved into the concept of stubs and how they serve as a practical method to isolate dependencies in tests. Here are the key takeaways:

  • Stubs offer a way to replace external dependencies by providing predefined return values, facilitating the testing of functionalities reliant on services beyond our immediate control.
  • Through the Red-Green-Refactor cycle, we emphasized writing tests that initially fail, implementing just enough logic to pass the tests, and refining the code consistently.
  • Using Mockito in Kotlin allows us to create stubs and mocks efficiently, leading to cleaner and more maintainable tests.

Prepare to apply these techniques in forthcoming exercises, where you’ll explore different scenarios utilizing stubs. This will enhance your skills in employing test doubles and adhering to the TDD methodology, ultimately advancing your ability to create dependable, thoroughly tested code.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal