A unit test generally tests the functionality of the smallest possible unit of code (which might be a method or a class). This article defines how we implement unit testing flow to our application architecture.
Fundamentals of Testing and the Testing Pyramid
As you can see from the Testing Pyramid, there are three categories of tests that can be included in any app’s test suite.
We investigated and decided to include tests from each layer. After the research, we decided to start by writing unit tests. First of all, we try to find out the basic concepts, what are the steps to be followed when writing a test.
Fundamentals of Trendyol Android
At Trendyol, we try to follow best practices and recommended architecture to build a sustainable and less error-prone Android app. We try to follow the rules from Architecture guidelines recommended by Google. Our architecture has simply 3 layers as mentioned : UI Layer, Domain Layer, Data Layer.
More detailed explanation can be found in the shared link :
Unit test code location
Test code location should be under /src/test/java/ui or /src/test/java/domain. We always have to be sure put all of our local unit tests here so that Gradle will know how to run them on JVM only.
Unit testing and our Application Arch.
Before we started to write unit tests to the classes in our codebase, we tried to decide which classes we need to write unit test. After a certain exchange of ideas, we decided the classes to be tested should be: ViewModels, ViewStates, UseCases.
We use ViewModel from AAC to store and manage UI-related data and LiveData to update the UI every time there’s a change. Therefore, we need to make sure that the correct state changes occur at the right time during remote/local data request.
When we are observing a LiveData inside a ViewModel that contains result of our request, we should differentiate between loading, error state, an empty list and success state. ViewStates are responsible for reporting changes to the UI.
We have simply three states :
Our ViewState classes have another job. We use The Data Binding Library to bind UI components in our layouts to data sources in our app using a declarative format rather than programmatically.
We need to make sure that our ViewState methods return the correct values.
Unit testing scenario
Given -> When -> Then
Fetch order contract successfully and show success state
A basic template for each layer.
Think of all the possible test cases.
Every unit test must start with @Test annotation
Divide the test -using comments- into three sections given for preparation, when for execution, then for verification.
How to test inside a ViewModel
We generally make a network request or make database connection inside ViewModels. We always make the correct state changes occur at the right time during remote/local data request.
This rule swaps the background executor used by the Architecture Components with a different one which executes each task synchronously.
val instantExecutorRule = InstantTaskExecutorRule()
This rule is necessary since we use RxJava in ViewModels
var testSchedulerRule = RxImmediateSchedulerRule()
observeForever method adds the given observer to the observers list. the given observer will receive all events and will never be automatically removed. This means that observer is always active.
How to test inside a ViewState
We expose the information about the state of our data using a wrapper that is called ViewState. As said earlier, We need to make sure that our ViewState methods return the correct values.
Create more readable assertions with Truth
We use assertion library called Truth as an alternative to JUnit based assertions.
We use a small library that provides helper functions to work with Mockito in Kotlin.
When we write unit tests, we use mock objects to mimic the dependencies of logic hosts outside the unit. We do the verification of their interaction as proof that they are being executed, because they do not provide implementation. Most of the time, classes have more than one logic and more than one dependency. Therefore, it is also important to verify that there are no unexpected interactions as much as interaction verification. For this reason, we use a utility called mock collector:
Detailed description for “verify no more interactions” check automatically for all mock objects (works with Mockito version 2)
Where To Go From Here?
- after completing the unit test writing progress, start to write integration tests,
- investigate how to use Mockk -a mocking library for Kotlin-
- start to improve our codebase for UI tests in our application
We have shared a repository about Android guidelines on Trendyol Android app.
I hope we helped you with this article, if you have other questions, please let us know! If you have liked this article, don’t forget to 👏 it. Thanks!