loading...

How to Write Unit Tests for Kotlin

bugfenderapp profile image Bugfender Originally published at bugfender.com on ・5 min read

Writing tests is an underappreciated part of software development. It usually distracts us from our primary goal and makes us feel we aren’t being productive.

However, these tests are a great way to ensure our code works as expected, and they can save us plenty of headaches when we don’t have full control of certain parts of a project (for example, when we are working on the client app and another colleague is working on the server).

Think of a basic task, like implementing the mapper between a database model object from the server, a business model object from the core of the system and a UI model. It is unlikely that the same programmer will write all three models. And when we don’t have control over a certain layer, that layer can change drastically without prior warning and our app might crash because of something as simple as a slightly different date format.

By implementing a test in this instance, we can give ourselves a better chance of detecting the error before going into production (and even save ourselves from being blamed by the client or colleague).

But writing successful unit tests involves a series of complex steps. So, in this article, we’re going to drill down into the disciplines required. For the purpose of this post we will aim to test a Rsync Collector class, which simply synchronizes files into a server.

Concepts

Let’s review a few basic concepts before going into the heavy code:

Behavioral Testing

Behavioural testing is so-named because we want to test interactions over objects instead of discovering the state of them. In other words we want to test how they behave, not how they are (otherwise they would be called state tests).

Test Doubles

In order to ascertain that certain bits of code work, it might be necessary to provide an intermediate object. For example, in order to test our RSync Collector we might need to simulate the behaviour of a server.

To solve this problem, software development guru Gerard Meszaros introduced the “Test Double” concept in his book XUnit Test Patterns: Refactoring Test Code. Meszaros uses this as a generic term for any kind of pretend object used in place of a real object for testing purposes (fun fact: the name comes from the notion of a stunt double in movies).

There are five principal kinds of test doubles: Dummies, Fakes, Stubs, Spies and Mocks. In this post we are going to talk exclusively about Mocks because they are the most common object to test in Android, and hence we’ll be referring to Kotlin.

Mocks

I like the definition provided by Martin Fowler for mocks, which describes them as _“_objects pre-programmed with expectations which form a specification of the calls they are expected to receive.”

We can think of a mock as a fake object that holds the expected behaviour of a real object but without any genuine implementation. For example, we can have a mocked server that looks like a server but simply sends a specific json object when calling a specific endpoint.

If we are creating a login service and we want to test the login flow, assuming that we are using a correct username and password, we may send a mocked ‘OK’ response throughout the server (like a json with some information, such as a user identifier).

Creating mocks might look easy and trivial, but it becomes a major undertaking as the project grows, and that’s why several libraries have been established to create mocks of objects.

Mockito is one of the most famous, and it can be complemented with Mockito-Kotlin, which provides us with Kotlin DSLs for the mock generation (here’s a great article talking about DSLs for those who aren’t familiar with the concept).

Here is an example of how to mock a native java File with Mockito-Kotlin:

Mocked File

The explanation is easy. It can be humanly translated as:

“Create a File that returns /random as an absolute path. For all other things provide a dummy implementation.” A dummy implementation, in case you’re wondering, does nothing but track the behavior of the object.

Another example:

Imaging a CollectorExporter class that needs to use a Context as parameter. Context is a really complex class provided by the Android framework and is something we don’t have control of. But we can easily create a mock, so we can track interaction and call its internal functions.

This is what a CollectorExporter looks like:

A Collector Exporter

And this is how we can create an instance of a collector exporter, using mocks

Collector Exporter initialized with mocks

As you can see, we are not mocking CollectorExporter (the object that syncs data with the Rsync server) but rather the context and the RsyncClient.

Let’s see the CollectorExporter in a big picture:

class CollectorExporter constructor(context: Context,
                                    private val executor: AppExecutor,
                                    private val sync: Sync = SyncClient(context)) {

  fun export(analyticsFolder: File, metadataFolder: File, collectorNetworkAddress: String, executor: ListeningExecutorService = this.executor, uploadFolder: String):
      ListenableFuture {

    return executor.submit(Callable {
      sync.sync(analyticsFolder.absolutePath, collectorNetworkAddress, uploadFolder)

      sync.sync(metadataFolder.absolutePath, collectorNetworkAddress, uploadFolder)
    })
  }

  fun export(file: File, collectorNetworkAddress: String, executor: ListeningExecutorService = this.executor, uploadFolder: String): ListenableFuture {
    return executor.submit(Callable {
      sync.sync(file.absolutePath, collectorNetworkAddress, uploadFolder)
    })
  }
}

We have two public export functions, primarily to export only analytics or analytics and metadata. In this case we want to prove that if we export one single file, rsync is called once, and if we export both then it will be called twice.

So, basically, a mock is a fast way to create an instance of an object for our tests without all the boilerplate involved in the execution of the code (instantiating a real server, using a real log-in,…) .

Verifications

Finally, a verification function can help us check that the object we’ve been testing behaves as expected.

Now that we have our objects created and instantiated, it’s time to create our test. We can check the behavior of an object by asking questions like how many times a function has been called or whether something has thrown an Exception. Our mocked objects will help us with that, as you’ll see in this script:

@Test
@DisplayName("When Exporting only one file then rsync#sync should be called only once")
fun exportServerCallRsyncSync() {
  collectorExporter.export(mockedFile, serverAddress, DirectExecutor, "").get()
  verify(sync, times(1)).sync(absolutePathMock, serverAddress, "")
}

We can see that calling a function called verify, passing as a parameter, a mocked object and a verification function (in this case a function that can specify the number of times a function has been called) we can check that, in fact, we are witnessing the expected behavior. In this case, when we call export specifying just one file (the network one) the rsync object that forms part of the CollectorExporter (and its mocked version) has been called only once.

Verifications are quite powerful and provide us a wide range of checking options. With Mockito, each verification is represented as a VerificationMode interface and it’s definitely worth taking a look into the default implementations to see all the possible options we have, like timeouts, exception thrown, and the number of times a function is called.

Do you want to learn more?

Unit testing is just a small piece of the high quality development puzzle that helps you before going into production.

When beta testing and even once in production, you can use Bugfender to remotely debug your apps and offer good customer support to your users. Don’t forget to take a look at our blog as we publish technical articles regularly.

Posted on by:

bugfenderapp profile

Bugfender

@bugfenderapp

Get fast remote access to your applications’ log files on users’ devices - wherever they are in the world. Bugfender logs virtually everything, and goes beyond simple app crashes. It even logs when the device is offline.

Discussion

markdown guide