Disclaimer: It has been brought to my attention the title can be seen as a click bait. That wasn’t my intention and I’m sorry. I wanted to reference the Fallout game series and the No Mutants Allowed community.
Testable code plays a crucial role in app development. When we neglect designing code for testability, we often resort to using a mock library (such as Mockito Kotlin, also know as “auto-mockers”) as a mean to achieve test coverage. Mocks have become a dominant presence in the Android testing ecosystem today. However, there are drawbacks:
- Brittle & Change-Detector Tests. Tests built with mocks often emphasise implementation details, causing them to break even when the system’s behaviour remains unchanged.
- False Sense of Security. High test coverage achieved through extensive mocking, can create a false sense of security. When all dependencies are mocked, we may overlook the fact that no real behavior is being tested.
- Slow Test Execution1. Reflection and proxies used by mock libraries can lead to sluggish test execution, slowing down the test suite when they increase in quantity.
The primary purpose of a mock library is to aid developers in their work. If a codebase relies solely on the presence of mocks for testability, it indicates potential design flaws2.
“There is a deep synergy between testability and good design. All of the pain that we fell when writing unit tests points at underlying design problems.” – Michael Feathers
In this article we’ll dive into how inversion of control using high-order functions or Functional Interfaces can help you achieve testable code without the needs for mocks. The key is to minimize dependencies, ideally reducing them to zero3. We should aim to eliminate dependencies on components like a class that is used throughout the application, or a shared data model that the System Under Test (SUT) doesn’t own.
Shared understanding: I’m using the word dependency to refer to a link between two functions, classes or modules. For simplicity, let’s say it is a direct import.
Now, let’s look an example4.
The Birthday Feature
Suppose we want to introduce a new feature in our app: a listing of employees’ birthdays. The feature has two requirements:
- Retrieve a set of employee records from a database.
- Display a list of employee records whose birthday falls on the current day.
To achieve this, we notice that we can reuse the existing EmployeeRepository
shared across the application. We only need to add a new method that filters employees based on their birthdate.
Here’s an example of the modified EmployeeRepository
:
class EmployeeRepository {
// New method!
suspend fun findEmployeesBornOn(month: Int, day: Int): List<Employee> =
findEmployees()
.filter { it.month == month && it.day == day }
// Old method, reused.
suspend fun findEmployees(): List<Employee> =
TODO("Load employees from a DB")
// 20+ unrelated methods...
}
And here is an example of its model:
data class Employee(
val id: EmployeeId,
val name: String,
val birthday: LocalDate,
// 20+ unrelated properties...
)
The view is already implemented, our task is to develop the ViewModel
that acts as the bridge between the View
and the Repository
.
A naive implementation of the BirthdayViewModel
could be as follows:
class BirthdayViewModel(
repository: EmployeeRepository,
) : ViewModel() {
private val _employeeBirthdays = MutableStateFlow(emptyListOf<Employee>())
val employeeBirthdays = _employeeBirthdays.asStateFlow()
init {
viewModelScope.launch {
val today = LocalDate.now()
_employeeBirthdays.value = repository
.findEmployeesBornOn(today.month, dayOfMonth = today.dayOfMonth)
}
}
}
However, this approach has a few issues:
BirthdayViewModel
depends on aRepository
that is used everywhere. If theRepository
changes, you need to modify theViewModel
. Except byfindEmployeesBornOn
, theViewModel
does not use any other method.BirthdayViewModel
depends onEmployee
, a data class it does not own. Except byemployeeBirthdays
, theViewModel
does not use any other property.EmployeeBirthdayViewModel
is unstable. It depends on bothEmployeeRepository
orEmployee
, and any change will directly affect it.- The usage of
LocalDate.now()
as a static function makes it challenging to replace during testing.
The following diagram provides a visual representation of the relationship between units:
Rather than coupling our feature with the EmployeeRepository
and Employee
data model, let’s aim for loose coupling and inversion of control.
Function as an Interface
Revisiting our requirements, we realize that we only need two things:
- Retrieve the names of employees whose birthday is today.
- Obtain the current day.
Kotlin’s support for high-order function allows us to treat any function as an interface. This concept enables us to achieve loose coupling5.
Here’s an improved version of BirthdayViewModel
leveraging a function as an interface6:
class BirthdayViewModel(
findEmployees: suspend () -> List<Employee>,
now: () -> LocalDate,
) : ViewModel() {
private val _employeeBirthdays = MutableStateFlow(emptyListOf<String>())
val employeeBirthdays = _employeeBirthdays.asStateFlow()
init {
viewModelScope.launch {
val today = now()
_employeeBirthdays.value = findEmployees()
.filter { it -> it.birthday.month == month && it.birthday.dayOfMonth == today.dayOfMonth }
}
}
data class Employee(val name: String, val birthday: LocalDate)
}
// For simplicity, will do all wiring in the factory.
// Keep in mind they can be placed in different files and/or Gradle modules.
// In a **real world project**, you will want to use Dagger Hilt or Koin Android for doing the wiring and bindings.
val BirthdayViewModelFactory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as MyApplication)
val repository = application.employeeRepository
// Pay attention in the `findEmployees` argument, that is the point of the article.
BirthdayViewModel(
findEmployees = suspend {
// Creates a map from `Repository.findEmployees` to `BirthdayViewModel.Employee`.
// Could be a class or high-order function if you want to test it in isolation too.
// The implementation here is NOT the point of the article, but a way to show the point.
repository
.findEmployees()
.map { it -> BirthdayViewModel.Employee(it.name, it.birthday) }
},
now = LocalDate::now,
)
}
}
This approach offers a few advantages over the previous implementation:
- During testing, it becomes straightforward to provide a trivial fake implementation of the
findEmployees
andnow
method. BirthdayViewModel
has access only to the specific function or property it requires.BirthdayViewModel
achieves stability since changes toEmployeeRepository
orEmployee
no longer directly impact it.- Both
BirthdayViewModel
can be tested in isolation without mocks or any complicated architecture, as easy as passing custom functions during your test set-up.
The following diagram provides a visual representation of the relationship between units:
Wrapping Up
In conclusion, relying excessively on mocks can lead to various pitfalls. By minimizing dependencies, we can achieve more robust and maintainable code. This architectural approach, known as “Ports & Adapters,”7 allows for interchangeable adapters, enabling different implementations for production and testing scenarios. Embracing testable design principles ensures that our tests accurately reflect the desired behaviour of the system, fostering a more reliable and efficient software development process.
If you want to learn more testing without mocks, here are a few links that can help you in your journey:
- Ports and Adapters Architecture
- Ports and Adapters / Hexagonal Architecture
- Testing on the Toilet: Do Not Overuse Mocks
- Testing on the Toilet: Test Behaviour, Not Implementation
- Testing on the Toilet: Change-Detector Tests Considered Harmful
- Jetpack: Do Not Mock
- Joe Blubaugh: Do Not Mock
- Fakes Are Great, But Mocks I Hate
- Testing Without Mocks
- How to Write Good Tests
Frequently Asked Questions
- That is an article about tests. Why is there, not a single test?
Good question. I did plan to write a before and after with tests, but the draft of my article got featured on Android Weekly #557 (?), and I really-really want to play Final Fantasy 16 so I guess this is now the final version.
- That is interesting but how does it look in practice?
One of the article’s reviewer has been kind, and shared a real use case from their production project:
- Should I do that for everything?
Software engineering is about trade-offs. I recommend you to follow the Principle of Least Power: Dependency Injection and build up as your requirements force you to introduce more complexity.
- You could achieve Ports & Adapters by introducing an interface to the repository. What is the advantage?
True. A single repository interface is less complex. But they are often shared between multiple features creating a coupling between the components of our system. That is not necessary a problem, it is a trade-off.
Credits
Special thanks to Jacob Rein, Stojan Anastasov, Fabricio Vergara, Thiago Souto and Guilherme Baptista proofread review! 🔍
And a special thank you to Niek Haarman for the Twitter thread that motivated me to write the blog post.
ℹ️ To stay up to date with my writing, follow me on Twitter or Mastodon. If you have any questions or I missed something, feel free to reach out to me! ℹ️
That is a stretch, I know. Any testing framework or library can introduce overhead. It’s not exclusive to mocks. ↩︎
See How to Write Good Tests for more examples. ↩︎
While it is important to minimize unnecessary dependencies, in practical scenarios, it is not always possible or even desirable to have zero dependencies. ↩︎
Birthday example is inspired by Birthday Greetings Kata. ↩︎
Alternatively, you can use a nested Functional Interface instead of a high-order function. That is useful when you need distinguished types, such as when using libraries such as Dagger or Koin. ↩︎
I know the example is a too simple, there isn’t much to test. But I hope you get the point. ↩︎
BirthdayViewModel
is the system,findEmployeeNamesBornToday
is a port, andcreateFindEmployeeNamesBornToday
creates the adapter. Mastering Ports & Adapters is a powerful skill which can be applied to any level of abstraction (functions, classes, or entire modules!) as you see fit. ↩︎