Android: unit testing tips & tricks
From a developer’s perspective, Android has come a long way from its early days when it was absolutely common to write all the code starting from UI stuff and down to low-level networking calls right inside Activities. It’s hard to believe now, but there were no architectural libraries like MVP-based Mosby or Moxy, and what’s even more important, Google itself had no public position regarding architecture at all: no official docs, no built-in tools, no official libraries, “everything-inside-activity” samples all over the place… Luckily, those days are gone and the modern Android development process is now much more mature than it used to be several years ago. Apart from other obvious advantages, this evolution made a good job of increasing the quality of an average app and Unit Testing is one of the very core things that made this possible. Today, when we tend to split application logic into a set of small components (like view models, repositories, and use-cases), more and more developers pay high attention to covering every component of their code with a decent amount of automated tests and I’m no exception🙂 In this post, I’d like to share some good practices, tips & tricks I’ve gathered in the last few years. Let’s get started!
Use assertion libraries instead of plain JUnit
Anyone who’s ever written a single test for Android should be familiar with the built-in set of assertions provided by Junit. Its core assertEquals
method is considered to belong to the “second generation” of assertion styles where the first generation is assert(boolean condition)
which is not really readable and not widely used nowadays. While assertEquals(expected, actual)
is definitely more convenient than assert(boolean)
when it comes to actual/expected validation, it is not ideal still. The first problem is that it’s easy to mismatch the order of parameters and put “expected” in “actual”’s place. This also affects readability as it’s hard to quickly get an idea of what we expect and what we actually get. The second problem is limited error output when some of the tests fail, the only hint on what went wrong is that 1 object was not equal to another. This may be acceptable for simple types like Strings
but not really convenient for things like Maps
& Lists
for instance. In order to solve all these issues, Junit4 added one extra method — assertThat(actual, matcher(expected))
. This format can be viewed as a third-generation matcher style and it greatly improves readability by replacing the old “assert expected == actual
” paradigm with “assert that ACTUAL matches EXPECTED
” + introduces 1 extra layer of custom matchers: we can now use type-specific validators (this improves error output in most cases) + it’s even possible to combine and group them using allOf(matcher1, matcher2,. ..)
or anyOf(matcher1, matcher2,. ..)
. So far so good, but… the second Matcher: matcher
parameter actually depends on the external Hamrest library which contains various implementations for this interface for different types. And what’s even more important, this method has been deprecated and Junit suggests using some 3-rd party matcher frameworks like Hamcrest which provide their own assertThat
implementations.
There are a lot of alternatives available:
Google unsurprisingly suggests their Truth here and there in the official docs, but my personal choice is Kotest, it works extremely well for Kotlin-only projects. Review the available options and pick one which works best for you, a good set of matchers can dramatically increase the readability of your tests compared to plain Junit.
Don’t abuse mocking frameworks when possible
Mocking frameworks like Mockito and Mockk are undoubtedly one of the core components of a modern testing suite. They’ve proved to be very useful when it comes to Behavior Testing (which is opposed to State Testing) but their power comes with a price: the “magic” they do works via tricky things like reflection and runtime code generation and manipulation. All of that brings extra overhead and makes your tests slower as you might expect so you should double-check that every use case cannot be properly implemented without those mocking libraries.
Let us review a couple of typical examples that may potentially be treated as “abuse”.
Case 1: Mocking simple “value” classes with no logic like DTO or POJOs:
data class User(
val firstName: String,
val lastName: String,
...more fields...
)
@Test
fun someTest() {
val user = mockk<User>(relaxed = true)
val result = objectUnderTest.getBalance(user)
…
}
Most of the time, you don’t do any behavior testing on such classes (like verifying that some property was invoked X times) so in fact, you don’t need to mock them. There are some rare cases when this can actually be useful (if you can’t access the constructor or if the class is just extremely hard to construct) but in many situations, mocking can easily be replaced with good-old constructors and real objects created specifically for tests.
Case 2: mocking simple interfaces that you don’t actually need to do anything and create mocks just to satisfy some dependencies:
interface Analytics {
fun onUserSignedIn(user: User)
}
class AuthViewModel(
val api: AuthApi,
val analytics: Analytics
) {
fun login(nickname: String, password: String) {
val user = api.login(nickname, password)
analytics.onUserSignedIn(user)
}
}
@Test
fun someViewModelTest() {
val analyticsMock = mockk<Analytics>(relaxed = true)
val api = ApiProvider.get()
val viewModel = AuthViewModel(
api = api,
analytics = analyticsMock
)
...
}
On top of the performance hit, uncontrolled mocking can also lead to other problems. Relaxed mocking (when all methods/fields of a mock return some default values like null automatically right after creation) often encourages a “lazy” or “mock and forget” test style. Let’s say somebody added a new method to the above Analytics interface. Our analyticsMock would still probably work, and while it may sound like a nice thing to have in some situations, in practice it is often a disadvantage. This mocking style masks important changes in the system, but ideally, changes in the public API should force you to update the tests to make sure they are still relevant at the very least. So “lazy relaxed mocking” in this case takes control out of your hands and disables compilation enforcement for important changes in the code under the tests.
Instead of creating a mock, consider creating some test implementation:
class TestAnalytis : Analytics {
fun onUserSignedIn(user: User) {
// Do nothing… or print user to the log, whatever makes
// most sense for your tests.
}
fun asSpy(): Analytics = spykk(this)
}
This object can further be turned into a Spy if you’ll ever need to do things like verify { (analytics.onUserSignedIn(any) }
.
Another good practice for larger teams is to have an agreement to implement test stubs for all important components of the code you or your team is responsible for. Such stubs usually provide a simplified implementation that can be easily controlled from the tests + they are often designed for simpler and more lightweight instantiation. Imagine a complex ShoppingCart class with a dozen of dependencies (…and each of them can also have its own dependencies too!) that is supposed to be re-used in many other features:
interface ShoppingCart {
fun content(): List<Item>
fun clear()
}
class ShoppingCartImpl(
val userRepostitory: UserRepository,
val shoppingCartDao: ShoppingCartDao,
val shoppingCartService: ShoppingCartService,
// 5, 10, 15 of other dependencies...
...
): Shopping Cart { ... }
In the code you’re working on, you just have a dependency on this class (let’s say you’re working on a View Model for a screen that renders the content of the cart + does some other things). Ideally, you’d want to be able to avoid creating a real object with a huge amount of dependencies — that’s probably too much since it’s just one of your dependencies and not the core logic of your feature. At the same time, it would be nice to make this dependency more or less functional so you can go through different paths of your own logic in the tests that is based on the different behavior of the class you depend on. In this particular example, you may need to test that the view state of your view model gets updated every time the shopping cart changes. It may be tempting to just create a mock that does something like this:
val content = mutableList<List<Item>>()
val mock = mockk<ShoppingCart>()
every { mock.content() } returns content
Well, it should do the trick but it will still be exposed to the 2 problems I mentioned above:
- The performance hit from mocking
- Changes in ShoppingCart like adding new methods will be hidden from you and your tests
As an alternative to mocking, the team that is responsible for this class can just create and maintain a test stub like this one:
class ShoppingCartTestStub(
val initialContent: List<Item>
) : ShoppingCart {
private val mutableContent = initialContent.toMutableList()
override fun content(): List<Item> = mutableContent
override fun clear() = mutableContent.clear()
// Bonus stuff!
fun addItem(item: Item) { mutableContent += item }
fun removeItem(item: Item) { mutableContent -= item }
}
This stub has some important benefits:
- It can be reused in the tests by anyone on the team who depends on ShoppingCart
- It should be maintained by the responsible team: every time they change the interface, they will also do you a favor and update the stub as well.
- People responsible for this interface know it the best so most likely nobody else is more geared for writing and maintaining a decent test stub.
Focus on the public API of the class under test
This one is simple a short: don’t try to make all the implementation details of your class visible just to make them accessible from tests. In general, your tests should only trigger the public API of your class (since that’s how your class will actually be used within the app) to verify all possible scenarios, use workarounds like internal
modificator in Kotlin or @VisibleForTesting
annotation only as the last resort. The motivation is pretty straightforward: unit tests that are focused on the public API are much easier to support, extend & maintain. Changing implementation details of the class under the test ideally should not make your tests fail, such tests are REALLY hard to maintain and have to be updated or even rewritten every time you change the internals of the class even if its public API remains unchanged.
Singleton is still an anti-pattern. Even in Kotlin.
Singleton
has been considered an anti-pattern for ages and it’s especially harmful to unit testing: apart from other issues like Dependency Inversion principle violation, Singletons may lead to unpredictable “class not mocked” runtime exceptions + they require special care when mocking. And with Kotlin which has completely replaced Java in new projects, things are surprisingly getting worse: while in Java you had to spend some notable time to implement a proper Singleton, all you need to do in Kotlin is to replace class
with object
and you’re basically done. This simplicity makes Singletons hard to resist in many situations but unfortunately, they will bring the same set of issues as in the case of plain-old Java. So, the suggestion is still the same: avoid Singletons in your codebase as they’re making testing harder. At the very least, consider passing your object
s as constructor parameters instead of implicitly using them from the affected class. If you’re using some DI framework like Dagger, Hilt, or Koin, replace your singletons with regular classes and make sure that your dependency graph will never contain more than 1 instance of the class. For instance, in Dagger, you can achieve this by marking your class with @Singleton
annotation.
“Treat tests as first-class citizens of the system”
The chapter related to Unit-testing has always been one of my favorite parts of the famous Uncle Bob’s “Clean Code” book and essentially it recommends to “treat tests as first-class citizens of the system”. It sounds pretty reasonable and straightforward but yet I’ve seen a lot of developers writing top-notch production code in accordance with all possible & impossible standards & best practices but completely ignoring the same rules when it comes to tests. For instance, one good developer will always try to avoid duplication of code in the “regular” code but for some reason, he or she can easily copy-paste hundreds of lines of code (test data, test utils) between a couple of test files that reside in a single package or directory… Care about the quality of your test code as much as you care about everything else. Eventually, your unit tests will be read & maintained by other developers so they must follow the same rules as regular production & user-facing code.
BONUS: Android Studio live template
Writing test methods following some of the popular formats like Given-When-Then
can become annoying when you write a lot of tests:
@Test
fun `test description here`() {
// GIVEN
// WHEN
// THEN
}
Luckily, Android Studio and IntelliJ IDEA come with a nice feature called “Live template”. There are some already available out of the box, but what’s even better, you can define your own templates from Settings -> Editor -> Live Templates:
Now, just start typing test...
and voilà!