Mocks, stubs, fakes, dummies, and spies on Android: from theory to (good) practice
In the first part of this series, we learned the theory involving the usage of test doubles. Now let’s cover how to use them in the Android world with some examples!
How to use test doubles on Android
In case we would like to create test doubles manually, there’s not much of a secret on Android. You just need to implement them correctly following the real dependency contract (like in most platforms or languages), and replace the real collaborator with the fake one. Dependency injection techniques can help us with this task.
Distinctions from other technologies start to emerge when we talk about tools. In the Android world, we can use frameworks that are compatible with Java and Kotlin languages to create our doubles. Among the most famous we can mention Mockito (for Java) and MockK (for Kotlin).
Below some examples using MockK:
@Test | |
fun `Retrieve notes count from server when requested`() { | |
val notesApiStub = mockk<NotesApi>() //Stub Double | |
val noteRepository = NoteRepository(notesApiStub) | |
val note = generateDummyNote() //Dummy Double | |
every { notesApiStub.fetchAllNotes() } returns listOf(note, note) | |
val allNotesCount = noteRepository.getNoteCount() | |
assertEquals(expected = 2, actual = allNotesCount) | |
} |
Configuring a Stub using Mockk
@Test | |
fun `Track analytics event when creating new note`() { | |
val analyticsWrapperMock = mockk<AnalyticsWrapper>() //Mock Double | |
val noteAnalytics = NoteAnalytics(analyticsWrapperMock) | |
noteAnalytics.trackNewNoteEvent(NoteType.Supermarket) | |
//Observes that specific operation has happened | |
verify(exactly = 1) { analyticsWrapperMock.logEvent("NewNote", "SuperMarket") } | |
} |
Configuring a Mock using Mockk
Doubles and maintenance costs
As mentioned in the previous article all test doubles can be configured manually or by external tools. Bear in mind that when we use external tools such as MockK or Mockito for this purpose, although they are easier to configure initially, they tend to couple our test code with implementation details from the dependencies.
Explaining with more details. The theory says that if we are testing a behavior, any modification that doesn’t change this behavior (let’s say a refactor) shouldn’t break the test code.
@Test | |
fun `Retrieve notes count from server when requested`() { | |
val notesApiStub = mockk<NotesApi>() //Stub Double | |
val noteRepository = NoteRepository(notesApiStub) | |
val note = generateDummyNote() //Dummy Double | |
every { notesApiStub.fetchAllNotes() } returns listOf(note, note) | |
val allNotesCount = noteRepository.getNoteCount() | |
assertEquals(expected = 2, actual = allNotesCount) | |
} |
Configuring a Stub using Mockk
In the example above, doing a refactor in the NoteApi
code, such as changing the fetchAllNotes()
method to receive a new parameter, shouldn’t break the NoteRepository
test… but unfortunately in the example above it will break, as the test shares NoteApi
implementation details as part of the MockK configuration (Line 6). If you imagine multiple tests doing the same configuration in multiple parts of the code, we could potentially have multiple broken tests due to a class refactor, generating overhead and maintenance costs on every refactor.
On the other side of the coin we can find the manually-configured doubles, or doubles that use the same contract as the real class for their setup. They have the drawback of potentially increasing the code base size in the beginning, but also have the benefit of not leaking implementation details to the tests as they don’t depend on any tooling magic. This can potentially improve the maintainability in the long run.
Below a comparison on how different types of double configuration can impact your code base maintainability:
Non-scientific comparison of test maintanance costs as the codebase size increases
Based on the above, for big code bases, my recommendation is to avoid configuring your doubles with tools that make you leak implementation details. This is counter-intuitive as you may think you are reinventing the wheel by not using those tools, however every new framework/library added to you project will imply in a trade-off, so it’s always good to know what the impact is and how to use the tool appropriately.
One important thing to mention is that I am not advocating against the tools, but against their misuse, as mentioned in the tweet below:
Depending on the scenario, you might consider using the sociable testing school to reduce the test double boilerplate code as well. As described in the tweet below:
In the following section, we are going to see some test doubles strategies that could be used in your Android code base, those strategies can be applied independently of the double configuration you choose (manual or via tooling).
Utilization of test doubles in non-instrumented tests
At the test pyramid’s base we find the non-instrumented tests, which are tests that don’t use emulators or real devices during their execution. The tests in this group are known to have a high degree of isolation and speed, therefore the usage of doubles will be very frequent here.
To demonstrate the usage of test doubles in the Android world, let’s see what an application would look like in a MVVM architecture:
Example of MVVM architecture. In green we can find the components that usually depend on the Android platform (View and Local Data Source) to execute their tests. In blue the components that usually do not depend on the Android platform (ViewModel, Repository and Remote Data Source) to execute their tests.
In my experience, I’ve seen the school of solitary testing to be the most common in Android code bases and in most scenarios, you are going to work on a project that already uses (or misuses) test doubles. So I’ll try to demonstrate a testing strategy that makes use of the school of solitary testing and how can you possibly improve your current setup:
Example of how to apply test doubles using MVVM in the non-instrumented layer.
Job Offers
In this strategy above, we use the following reasoning:
Testing the View
View layer tests are not very common in the non-instrumented part. For us to create them we will probably need to build a Robolectric test, which is known to be slower than pure Java/Kotlin tests.
In case you test your Views, as we are dealing with an expensive test, my recommendation is that we don’t use doubles for the ViewModel, but for the Repository. The reason is because ViewModels (by design) are components tightly coupled with Views and therefore the behavior of the View would be better represented if we used real ViewModels. Also, we wouldn’t have much gain in speed if we used a double for the ViewModel, this test would be slow anyway. In this specific example, the Repository would be well replaced by a Stub.
Testing the ViewModel
To test the ViewModel layer, we can do an Android-free unit test and replace the Repository with a Stub, straight to the point. Other ViewModel’s dependencies could also be replaced by test doubles.
Testing the Repository
To test the Repository layer, we can also do an Android-free unit test. Here, we replace the Local Data Source and the Remote Data Source with Stubs or Fakes.
Testing the Remote Data Source
To test the Remote Data Source layer, we will probably have to replace the backend with some double. The easiest way would be to create a Fake using the MockWebServer tool, which will simulate an HTTP server responsible for returning pre-configured responses. Tests in this layer are useful to assert that the serialization of the backend responses is working correctly.
Testing the Local Data Source
To test the Local Data Source layer, we will probably have to replace the local database with a Fake or Stub. If we want to do this kind of testing on the non-instrumented layer, Robolectric could be an option as well. Even so, I believe that this type of testing might be more useful in the instrumented part of your test suite. To test the data layer in isolation, prefer to do it more reliably.
Keep in mind that this is just an example, if you have a well structured test double strategy and you use them well in your code base, you will have less trouble to switch to other test school or replace tools if you need to. Lastly, remember that the best approach will always depend on your team’s context.
Use of doubles in instrumented tests
The test doubles are also very important for the instrumented area of the pyramid, but they should be used a little bit more carefully. Loading Android resources and running tests using an emulator or a real device is certainly a costly operation, perhaps more expensive than the vast majority of non-instrumented tests.
Since we will have a higher cost to build this type of infrastructure, we should make the most of it and integrate as many real components as possible, and consequently, use less test double. One common mistake I see are UI tests replacing the ViewModels by doubles. In my opinion, the more expensive is the test, the closer you should get to reality, which means that more code you should try cover. By using doubles to replace ViewModels in UI tests will simply lead to a complex and slow test with low code coverage.
Also, another point of attention is that most tools (like MockK or Mockito) manipulate bytecode to create Mocks and Stubs. As Android uses its own virtual machine and generates bytecode with a specific format (Dalvik), some of these tools have limitations to create doubles in this instrumented layer. For those reasons, I advise against using stunt doubles for instrumented tests. With the exception of two situations:
- To replace dependencies on I/O boundaries, such as backends and databases, with faster, more deterministic doubles. Making use of tools like MockWebServer for example.
- Or to replace third-party tools that are difficult to configure in instrumented tests, such as Firebase classes.
Test double strategy in instrumented tests
Dependency injection tools such as Dagger, Hilt or Koin, and Product Flavours can be great allies when using test doubles in the instrumented layer.
Android community and test doubles
As we saw in the previous sections, we have doubles that have similar purposes but are implemented and configured in different ways. Discussions about how you should use doubles and the trade-offs around configuration simplicity and maintenance have brought many interesting debates and publications in the development community.
The relationship between test fragility and refactoring is one of the most common topics you will see. Just some examples 👇
Conclusion
You should now be able to answer, or at least understand, the questions asked in the introduction of the first article:
- “We need to mock this dependency and everything will work fine” 🙌
- “Avoid using Mocks!” 😱
- “Mocks vs Stub?” ⚔️
- “Prefer using Fakes than Mocks” 🤔
Test doubles (also generically referred as Mocks) are very important to your testing strategy. Get to know their concept well, understand their trade-offs and with that, keep testing your Android application in a scalable and easy way.
If you learned something from this article, leave your claps 👏 So I can know that it was helpful to you. Any feedback or questions, send them on Twitter or in the comments of this post. Thanks!
References
- XUnitPattens by Gerard Meszaros
- Test double definition by Martin Fowler
- Unit test definition by Martin Fowler
- Mocks Aren’t Stubs by Martin Fowler
- Mocks and Stubs aren’t Spies by Hamlet D’Arcy
- Unit Testing: Mocks, Stubs and Spies Gabo Esquivel
- Testing on Toilet by Google
- Replacing Mocks by Ryan Harter
- Prefer Fakes Over Mocks by Alexey Golub
This article was originally published on proandroiddev.com on May 23, 2022