The more observant regular readers of Styling Android will be aware that recently all of the sample code has switched from Java to Kotlin. There are many aspects of Kotlin which make our lives as developers much easier and recently I have been exploring how to make Kotlin classes easier to test. In this short series we’ll take a look in to some techniques which will can enormously assist in making our Kotlin classes testable.
A major thing which can inhibit testability is classes which create collaborators – i.e. they create other objects. This becomes problematic because it means we cannot test the object in isolation from other objects – so we cannot truly unit test any classes which create their own collaborators. Another problem that this can introduce is where these collaborators are Android Framework classes which will be stub implementations which will not behave correctly during our test runs. The obvious solution to this is to mock these objects, but we cannot do that if the class under test actually creates these objects itself. This is not an issue specific to Kotlin or even Java – it is a general principle of software development.
The solution to this is simple in principle, but can be a little trickier in practise. In many cases we can use dependency injection either using a Dependency Injection framework such as Dagger, or by manually implementing constructor or setter injection (this is described here). That’s fine if there are a fixed set of collaborator instances for the object, but if it needs to dynamically create collaborator instances this will not work. The pattern that I prefer to use in these instances is to use a Factory which is responsible for creating object instances, and the Factory itself can be injected.
Rather than all of this dry explanation, let’s look at an example which demonstrates this:
class DateStringProvider { private val formatter: DateTimeFormatter by lazy(LazyThreadSafetyMode.NONE) { DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL) } fun buildDateString() = formatter.format(LocalDateTime.now()) as String }
Here we have a class which is responsible for providing a formatted String of the current date & time (it uses Jake Wharton’s ThreeTenABP JSR-310 back-port optimisation).
This looks to be pretty easy to unit test and we can write a JUnit 5 test for this:
class DateStringProviderTest { @Nested @DisplayName("Given a DateStringProvider instance") inner class DateStringProviderInstance { val testableClass = DateStringProvider() @Nested @DisplayName("When we build a date string") inner class BuildDateString { private val dateString = testableClass.buildDateString() @Test @DisplayName("Then it should not be empty") fun notEmpty() { assertThat(dateString).isNotEmpty() } @Test @DisplayName("Then it should equal \"$DATE_STRING\"") fun isEqual() { assertThat(dateString).isEqualTo(DATE_STRING) } } } companion object { const val DATE_STRING = "Date String" } }
This all looks as though it should work, but if we execute the test it fails:
org.threeten.bp.zone.ZoneRulesException: No time-zone data files registered at org.threeten.bp.zone.ZoneRulesProvider.getProvider(ZoneRulesProvider.java:176) at org.threeten.bp.zone.ZoneRulesProvider.getRules(ZoneRulesProvider.java:133) at org.threeten.bp.ZoneRegion.ofId(ZoneRegion.java:143) at org.threeten.bp.ZoneId.of(ZoneId.java:357) at org.threeten.bp.ZoneId.of(ZoneId.java:285) at org.threeten.bp.ZoneId.systemDefault(ZoneId.java:244) at org.threeten.bp.Clock.systemDefaultZone(Clock.java:137) at org.threeten.bp.LocalDateTime.now(LocalDateTime.java:152) at com.stylingandroid.mylibrary.DateStringProvider.buildDateString(DummyDateStringProvider.kt:13) at com.stylingandroid.mylibrary.DateStringProviderTest$DateStringProviderInstance$BuildDateString.<init>(DateStringProviderTest.kt:18) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at org.junit.platform.commons.util.ReflectionUtils.newInstance(ReflectionUtils.java:342) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:80) at org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor.instantiateTestClass(NestedClassTestDescriptor.java:68) at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.instantiateAndPostProcessTestInstance(ClassTestDescriptor.java:189) at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$testInstanceProvider$1(ClassTestDescriptor.java:182) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:80) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:60) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.lambda$null$2(HierarchicalTestExecutor.java:92) . . .
Those that are familiar with the library may already know the cause of this: The library needs to be correctly initialised before it can be used – Jake’s optimisation is to change how the timezone data is loaded, and this requires initialisation in the Application object. For our unit tests there is no Application object, so we get this error. The obvious solution would be to call the initialisation method before we run our tests. However this actually loads assets from the APK, so this will fail if we run it from the test framework.
There are a couple of ways around this. Firstly, for test builds we can use the pure Java implementation of ThreeTen which Jake’s library derives from. This is arguably the best solution, but I will offer some alternatives because they include some techniques which may be of use in other scenarios – specifically the case of stub implementations of Android Framework classes.
The first option would be to inject the DateTimeFormatter and LocalDateTime objects:
class DateStringProvider(private val formatter: DateTimeFormatter) { fun buildDateString(localDateTime: LocalDateTime) = formatter.format(localDateTime) as String }
This immediately solves the problem because we can now pass in mocks for these two objects. However, I don’t like this solution because we are now exposing specifics of our implementation. In the original code, and consumer of an instance of this class was completely agnostic that we were using ThreeTenABP internally; but with our ‘fixed’ version this detail is exposed and requires more effort by the consumer – it needs to create the objects in oder to use this class. So by making the class testable we have really dirtied the API for this class.
A better solution to this is to use a Factory. The Factory is responsible for creating these instances:
class DateStringProvider { fun buildDateString() = Factory.getLocalisedDateTimeFormatter().format(Factory.getLocalDateTime()) as String private object Factory { fun getLocalisedDateTimeFormatter(): DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL) fun getLocalDateTime(): LocalDateTime = LocalDateTime.now() } }
That shows the basic principle of separating the object creation away from DateStringProvider, but this does not help because the object creation is still self-contained within DateStringProvider so we cannot create mock instances.
In Java we would generally have to inject the factory to enable us to use an alternate factory for testing – this alternate factory would enable us to provide mock objects. This is how I would normally overcome this issue, but it does muddy the API slightly because we need to inject the factory instance before we can use an instance of this class.
However with Kotlin we can nicely mask this:
class DateStringProvider internal constructor(private val factory: Factory) { constructor() : this(DefaultFactory) fun buildDateString() = factory.getLocalisedDateTimeFormatter().format(factory.getLocalDateTime()) as String internal interface Factory { fun getLocalDateTime(): LocalDateTime fun getLocalisedDateTimeFormatter(): DateTimeFormatter } private object DefaultFactory : Factory { override fun getLocalisedDateTimeFormatter(): DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL) override fun getLocalDateTime(): LocalDateTime = LocalDateTime.now() } }
Here we’ve added an interface named Factory and have a private object named defaultFactory
which implements this interface. We then have two constructors. The primary constructor takes a factory argument, and a secondary default constructor which will initialise the object using defaultFactory
.
A consumer of this class only needs to call the default constructor – DateStringProvider()
– and it gets a perfectly functional instance of this class without having to perform any injection whatsoever. But when we come to test, we can use a different factory:
class DateStringProviderTest { @Nested @DisplayName("Given a DateStringProvider instance") inner class DateStringProviderInstance { val testableClass = DateStringProvider(Factory) @Nested @DisplayName("When we build a date string") inner class BuildDateString { private val dateString = testableClass.buildDateString() @Test @DisplayName("Then it should not be empty") fun notEmpty() { assertThat(dateString).isNotEmpty() } @Test @DisplayName("Then it should equal \"$DATE_STRING\"") fun isEqual() { assertThat(dateString).isEqualTo(DATE_STRING) } } } object Factory : DateStringProvider.Factory { override fun getLocalDateTime(): LocalDateTime = mock {} override fun getLocalisedDateTimeFormatter(): DateTimeFormatter = mock { on { format(any()) } doReturn DATE_STRING } } companion object { const val DATE_STRING = "Date String" } }
What this does is provide our own factory implementation when we instantiate the DateStringProvider object, and this returns mock instances of the collaborators.
As an interesting side-note – the primary constructor specifies internal
visibility which limits the scope to the module where it is implemented. The sample project source actually implements this in a separate library, and from the main app codebase, this primary constructor is not visible. This helps keep our external API particularly clean as no details of the use of ThreeTenABP are exposed, and neither is the factory.
Further to this: The advice coming from Google is to modularise your builds in to separate libraries to speed up build times. From Android Gradle plugin 3.0.0 and later only the modules which have changed will be built, so there are performance benefits from doing this, as well as getting the full benefit of Kotlin’s internal
visibility.
I refer to this pattern as the “Default Factory Pattern”. Any class can define its own Factory and provide a default factory implementation which will be used in production builds, but an alternate factory can be provided for testing the object in isolation. It requires only very slightly more work, but really simplifies testing. I have not seen this specific pattern documented elsewhere hence wanting to share it, but it would not surprise me to learn that others have though along the same lines.
So, with all of this in place, we’re good to go. Or are we? If we run this we get a different error:
org.mockito.exceptions.base.MockitoException: Cannot mock/spy class org.threeten.bp.format.DateTimeFormatter Mockito cannot mock/spy because : - final class at com.stylingandroid.mylibrary.DateStringProviderTest$factory.getLocalisedDateTimeFormatter(DateStringProviderTest.kt:82) at com.stylingandroid.mylibrary.DateStringProvider.buildDateString(DateStringProvider.kt:13) at com.stylingandroid.mylibrary.DateStringProviderTest$DateStringProviderInstance$BuildDateString.<init>(DateStringProviderTest.kt:23) . . .
The problem here is that DateTimeFormatter has actually been declared as final
and Mockito is not able to mock a final class. This is actually a common issue with Kotlin because by default classes are final
unless we explicitly declare them as open
. But mocking is something of a special case – sometimes it can be useful to mock a final
class and it is annoying if we have to leave classes which have not been designed for inheritance open to make them mockable – This can lead to problems further down the line.
However, Jetbrains have thought of this and there is a little trick that we can use which will make classes open but only for our test runs. Within our test source we need to add a file in resources/mockito-extensions/org.mockito.plugins.MockMaker
:
This needs to contain a single line:
mock-maker-inline
By adding this we can now mock final classes, and if we run our test suite again we finally have green lights:
So with a few small changes to our class design we can easily introduce a separation from collaborator instantiation without having to compromise on our API design. In the concluding article in this series we’ll look at how we can use this same principle to tackle some other cases specific to Android.
The source code for this article is available here.
© 2017, Mark Allison. All rights reserved.
Copyright © 2017 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
The link for manual dependency Injection is broken.
Thanks for letting me know. It should be fixed now.
Does the mock-maker-inline approach also work for injecting mocks in the androidTest/ source set? For example, I have a class (written in Kotlin) that encapsulates some functionality relating to Google’s auth API. I don’t want to make an actual auth call during an instrumented test. Other than creating an interface for this class, how would I be able to easily mock it?