In this blog, we will learn about:
- What is TDD?
- What is Unit testing?
- Why is it important?
- Introduce mockK.io
- Why is it better than a well known Mockito library?
- How to configure mockK.io?
- Explain how to set up or get started with writing unit tests
- Explain its Annotations used for unit testing
What is Test-Driven Development (TDD)?
TDD is a software development approach where tests are written before writing the minimum required code for the test to be satisfied. The code will later be refactored, as it needs to pass the test, with the process being repeated for each piece of functionality. The below figure shows how it works. In short, terms, write minimal code and refactor continuously to pass the test.
Test-Driven Development Process:
- Add a Test
- Run all tests and see if the any fails
- Update code
- Run tests and Refactor code
- Repeat the same process
To implement TDD, we will need Unit tests which focus on every small functionality of the application.
Let’s move to what is UNIT TESTING and why it is necessary.
What is Unit Testing?
Unit testing is a process in which the smallest testable parts of an application, called units, are individually and separately tested for proper operation. This testing methodology is done during the development process by the software developers.
A unit is a container of specific functionality that has clearly defined boundaries, which, in Android, we call it a Class. In unit tests, we can say that we make sure that the code written behaves correctly.
How the unit tests work?
A unit test typically comprises three stages: plan, differentiate cases involved, and the unit test. In the first step, the unit test is prepared and reviewed against the implementation. The next step is to identify for the test cases to be written, then last but not least, the code is tested.
Why is Unit Testing necessary?
Avoiding unit tests saves time in developing applications is not a proper approach. It is a false understanding because when the application is completed, skipping unit testing leads to higher risk and defect fixing costs. Unit testing performed during the development stage saves both time and money in the end, which results in a quality application.
Here are key reasons why to perform unit testing.
- Unit tests help to fix bugs at the early stage of the development cycle.
- It enables excellent help to the developers to understand the code base and enables them to make changes quickly and also help in space complexity and time complexity.
- Unit testing allows the programmer to have quality code and make sure the module works properly.
- Unit tests help with code reuse. Update the code until the tests run again.
- Unit Tests, when integrated with a build, give the quality of the build as well.
Here, we will be using mockK.io for implementing unit testing. So, let’s get started with mockK.io.
Introduction to mockK:
As we all are aware, how Kotlin is used extensively for developing android applications worldwide,
In kotlin, all classes and methods are final by default. But it causes some problems while writing unit tests.
Most JVM mock libraries have problems with mocking or stubbing final classes. We can add the “open” keyword to classes and methods that we want to mock. But changing classes only for mocking some code doesn’t feel like the best approach. By doing this, we are reducing the performance and which is not suitable for High-quality codebase.
In Android, there are many frameworks for mocking in unit testing, such as PowerMock, Mockito, etc. Mockito is a popular framework used by Java developers. With StackOverflow, we can find many solutions for mockito. But it does have some fundamental issues when used with Kotlin. MockK, being specially designed for Kotlin, is a more reliable and pleasant experience.
MockK is a better option for other mocking frameworks for Kotlin, the official development language for Android. Its support for Kotlin features comprehensively.
Why is it better than a well known Mockito library for Kotlin?
Mockk supports some important language features within Kotlin.
- Final by default(Classes and methods in kotlin) :
Concerning Java, Kotlin classes (and methods) are final by default. That means Mockito requires some extra things to make it to work, whereas Mockk can do this efficiently without any extra things. - Object mocking :
Kotlin objects mean Java statics. Mockito alone doesn’t support mocking of statics. There are the same other frameworks required with Mockito to write tests, but again Mockk provides this without any extra things.
mockObject(MyObject) every { MyObject.someMethod() } returns "Something"
3. Extension functions :
Since extension functions map to Java statics, again, Mockito doesn’t support mocking them. With Mockk, you can mock them without any extra configuration.
4. Chained mocking :
With Mockk you can chain your mocking, with this we can mock statements quite easily like you can see in the example below. We are using every{} block to mock methods.
val mockedClass = mockk() every { mockedClass.someMethod().someOtherMethod() } returns "Something"
5. Mocking static methods is not easy in mockito but using mockK java static methods can easily be mocked using mockStatic.
mockkStatic(TextUtils::class) @Test fun validateString() { every { TextUtils.isEmpty(param } returns true }
This is how you can mock static method isEmpty(param) of TextUtils class easily.
So, with those impressive features in mockk makes mocking in Kotlin great.
Now, we will see how to add mockk in our android application.
How to configure mockK.io?
Configuration is as simple as it can be. We just need to add the mockk dependency to our Maven project:
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.9.3</version>
<scope>test</scope>
</dependency>
For Gradle, we need to add it as a test dependency:
test implementation "io.mockk:mockk:mockk_latest_version"
Now, we will look into different annotations used for writing unit tests.
Before moving to annotations, let’s have a look into terms used in unit testing.
Mocking and Stubbing :
Mocking and stubbing are some essential terms involved in unit testing.
Mocking means making a fake or dummy version of a function or method that can work like the real one. By doing this, we can write and run unit tests speedily and more trustworthy. When our implementation communicates with that particular method or function, rather than original function or behavior, a mocked function will be used.
Stubbing means creating a dummy version, but a stub only mocks the behavior and not the entire object. It is used when our implementation only communicates with a specific behavior of the object.
Let’s discuss how we can use these methods to improve our testing strategy.
We use mocking or stubbing when your code uses external dependencies like network calls or accessing a database. When we are saying that we will create a dummy or fake version, it does not mean that we will write the same as the actual or original method implementation. If we do write the same as the original one, then there is no point in writing unit tests. Because it will take plenty of time to verify unit tests, we will just mock or stub the methods, and we will skip its implementation, so we can save time and produce efficient code. And also, we can mock or stub the results returned by network call or any database operation.
Test Environment
Now, we will get started with annotations used to write unit tests.
We have some essential annotations in JUnit5, which plays a vital role while writing unit tests for class.
Those methods are @Before and @After.
@Before
Methods annotated with the @Before annotation are executed before each test. It is beneficial when we want to execute some repeated code before running a test like we can mock objects or initiation required for annotations.
@After
Methods annotated with the @after annotation are executed after each test runs. It is useful when we want to unmock all the objects which we mocked.
Annotations :
For every test, we write there are 3 essential steps involved which are
1. Given
2. When
3. Then
We will look into this once we deep into tests.
For all the annotations to work on an object declaring a variable with annotation, we need to call MockKAnnotations.init(this, relaxUnitFun = true). Here, relaxUnitFun means relaxUnitFun makes it relaxed only for unit returning functions.
It initializes properties annotated with @MockK, @RelaxedMockK, @Slot, and @SpyK in the provided object.
We can call this method in the init block, here we will call as setUp(). We will use @Before annotation with this method.
@Before fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) }
@MockK
It builds a regular mock.
We can use this annotation in two ways.
By using annotation, we can declare a variable like the below example.
@MockK private lateinit var object: MyClass
and another way is by using mockk method
val object = mockk()
@Spyk
Spy allows mocking only a particular part of some class.
By using annotation, we can declare a variable like below example
@Spyk private lateinit var object: MyClass
And by using spyk method.
val object = spyk()
For mocking static method:
For mocking static methods, we will use MockStatic methods.
Let’s see an example to understand this method. For example, in “TextUtils.isEmpty” isEmpty is a static method so that we will write like
mockkStatic(TextUtils::class) will mock TextUtils class. Let’s discuss the example below.
Class StringChecker { fun isTextEmpty(text : String) : Boolean{ return TextUtils.isEmpty(text) } }
Now, we will write a unit test for the above class.
Class StringCheckerTest { lateint var stringChecker : StringChecker @Before fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) mockkStatic(TextUtils::class) } @Test fun stringChecker_Is_Empty() { //given stringChecker = mockk< stringChecker >() every { TextUtils.isEmpty(any()) } returns true // stubb the call to method //when val result = stringChecker. isTextEmpty(“test”) //then assert(result, true) } }
As mentioned above, 3 steps while writing the test, let’s discuss here. You can see in the above unit test method they are mentioned.
In the given block, we provide the requirements for writing unit tests.
In when block, we call the actual method form the class.
In then block, we verify whether the test is performing correctly.
Object & enumeration mocks:
For mocking objects, mockK has a simple method, which is mockkObject. It just takes the object as an argument, and it becomes a spy. With this method, we can use it as original works and can also stub and verify statements.
object ExampleObject { fun add(a: Int, b: Int): Int } mockkObject(ExampleObject) every { ExampleObject.add(4, 5) } returns 9
However, most of the mocking libraries have a problem with mocking Kotlin singleton instances. Because of this, MockK provides the mockkObject method.
Properties mocking:
Usually, we can mock properties as if it is get/set functions or field access. But if we have private property or access to the field, we can use alternatives for mocking explained below:
val mock = spyk(Car(), recordPrivateCalls = true) every { mock getProperty "speed" } returns 33 every { mock setProperty "acceleration" value less(5) } just runs verify { mock getProperty "speed" } verify { mock setProperty "acceleration" value less(5) }
Private functions mocking/dynamic calls:
In case you need to mock private functions, you can do it via a dynamic call.
class Car { fun drive() = accelerate() private fun accelerate() = "going faster" } val mock = spyk(recordPrivateCalls = true) every { mock["accelerate"]() } returns "going not so fast" assertEquals("going not so fast", mock.drive()) verifySequence { mock.drive() mock["accelerate"]() }
In case we want private calls to be verified, we should create a spyk with recordPrivateCalls = true.
Till now we have discussed some essential annotations of mockK.io
Now, we will take a real-time example to understand how to use mocking.
We will have the basic use case wherein we will test the class, which has business logic, which will perform some operation and post the results through the repository.
In this blog, we have used an MVP pattern which consists of three parts:
- Presenter who has the business logic(we will test this)
- View
- The repository will fetch data from API.
Presenter:
class MainPresenter( private val view: View, private val dataRepository: DataRepository ) : BasePresenter(view) { fun fetchData() { mvpView.showProgressDialog() dataRepository.getData(object : dataRepository.Callback { override fun onSuccess(data: List) { mvpView.hideProgressDialog() mvpView.setResult() } override fun onFailure(errorMessage: String) { mvpView.hideProgressDialog() mvpView.showErrorDialog(errorMessage) } }) } }
And the repository class:
class DataRepository { fun fetchData(callback:Callback) { dataManager.getData() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(organizationWrapper -> callback.onSuccess(data), throwable -> callback.onFailure(throwable.getMessage())); } public interface RxHelperCallback { void onSuccess(R response); void onFailure(String errorMessage); } }
It’s quite simple, after getData() we make two things: fetch and pass the result to the view.
Now, we will write tests for MainPresenter.
class MainPresenterTest { @MockK lateinit var view: View @MockK lateinit var dataRepository: DataRepository lateinit var mainPresenter: MainPresenter @Before fun setUp() { MockKAnnotations.init(this) mainPresenter = spyk(MainPresenter(view, dataRepository)) } @Test fun test_getData_Success() { //given val dataList = mockk>() val captureData = slot>>() // to mockk or stub the actual data fetch call every { dataRepository.getData(capture(captureData)) } answers { captureData.captured.onSuccess(dataList) } //when mainPresenter.fetchData() //then verifyAll { view.showProgressDialog() view.hideProgressDialog() view.setResult() } }
Here, we will see each term used for unit tests.
In this sample, we have two mocked parts: the repository and the view, on the first one we are mocking the data production with the every{} method to emulate a list of data or an output with a specific kind of data.
On the second, we are using another method: verifyAll{} to verify if a specific method is called (and how many times), in our sample, it’s the callback interface where we provide the presenter output.
You can see that we have mocked the data repository and view by using @MockK annotation.
In the setUp() method, we have initialized mockk annotations. We have used @Before annotation above this method so that it will execute before the actual test executes.
Now we will move to the test method “test_getData_Success()”.
Capture and Replay an Argument:
Here, we need to capture the parameters passed to a method; then, we can use CapturingSlot. It is useful when we want to have some custom logic in an answer block, or we just need to verify the value of the arguments passed.
val captureData = slot<Callback<List<DataModel>>>()
slot() : It creates a capturing slot means to store specific argument values used.
every { dataRepository.getData(capture(captureData)) } answers { captureData.captured.onSuccess(dataList) }
In the above code written there are some important methods which are every {}, capture(), answers {} and captured(). We will see one by one below.
For mocking or stubbing code or statements written in the actual method, we have used every block.
It can be used for mocking parameters or any statement or line in code.
In our example, we have mocked the fetch data call. And you can also see that we have used answers blocks as well. The answers block is written for returning results from the mocked method.
answers { code } Here, we mention calls or methods which are performed by code, which we have mocked. In this case “dataRepository.getData()” is mocked.
capture(slot) is used to capture value to a CapturingSlot for a parameter in the method.
captured() is used to capture the method. And, for example, we have captured the onSuccess method in our example.
Similarly, we can also capture onFailure methods and then verify accordingly.
So, we have completed the given block.
In the next block, which is when block, we call the function for which we have written this test function.
In the “then” block, we will verify statements whether they executed or not.
Methods to verify statements:
- verify{} - It verifies unordered a verification that a call performed.
- verifyAll{} - It verifies that only the specified calls executed for the mentioned mocks.
- verifyOrder{} - It verifies that the sequence of calls went one after another.
- verifySequence{} - It verifies the specified sequence of calls executed for the mentioned mocks.
Last but not least, we will unmock all the mocked objects by using the unmockkAll() method. We have used @After annotation above the tearDown() method to execute after test executes.
In this way, we have tried to explain annotations that are required to perform unit tests with examples.