Android: Understanding Spek Tests (Part 1)

26 September 2019

How can you improve the quality of your code if you don’t have a mature test strategy?

Joseph Magara (Android Developer at GrowthOps) gives us the lowdown on Spek testing and details his journey from testing novice to SubjectSpek evangelist.

My experience with testing

Early on in my career as a developer, I was what you could call a test enthusiast. When discussing tests and their importance with other developers, I was all for testing. However, in practice my code was test-free. This discrepancy wasn’t due to hypocrisy but rather ignorance. Though I could see the value of testing, I had no idea where to start and the tools I was aware of seemed complex to use. Since then, a lot has changed (including my testing) and now I genuinely practice and encourage testing. The purpose of this article is to help developers who may not be very comfortable with testing (specifically SubjectSpek testing) to get a handle on what Spek and SubjectSpek tests are and how they work.

Let’s dive in.

Benefits of testing

Tests have a multitude of benefits for the codebase - they verify that your code functions the way it’s intended and they preemptively finds bugs that would otherwise have snuck into your production and release builds. In addition to this, testing secures code for future maintenance by preventing future developers from breaking current functionality as they add new features. It also serves as in-code documentation of the projects’ explicit and implicit requirements/AC’s (Often new developers can review the tests to find out the requirements of the project).

What is Spek?

Spek is an open source, behaviour-driven testing framework written by JetBrains for Kotlin. It is a specification framework that allows you to easily define your tests in a clear, understandable, human readable way. SubjectSpeks are a feature provided by Spek tests to provide an easier way of writing these types of tests and to remove a lot of boilerplate code (explanation taken from Spek User Guide).

How do they Work?

Spek tests have three primary functions that we will focus on in this article (there are actually many more useful functions but we will cover those in later articles). These main functions are the group, the action and the test functions.

These functions — though exposed to the developer — are not normally directly used but rather, they are indirectly accessed by wrapper functions that help the tester write the tests in a way that is easy to read and understand (we’ll explore these later). Before we have a look at these functions and how they work, we’ll add the needed dependencies to the project and also create a use-case that we can test.

Adding the Needed Dependencies

Import the needed dependencies:

spek-1

Setting up the Testing

The Use-Case

We’ll set up a WeatherUseCase that has two functions:

  • getWeeklyWeatherForecast()
  • setupLiveUpdates()

For the sake of this test, we’ll say that live updates are only available on newer Android phones that support backgrounded pub-sub functionality.

The use-case will have two dependencies:

  • A WeatherService: This is the class what will actually make the rest calls to get the weather forecast and to subscribe for live updates
  • A SupportsLiveUpdates boolean flag: This will be used to determine if subscription to live updates can be done.

So here is our WeatherUseCase:

spek-2

Now that the use-case is set up, let’s start setting up the tests.

Test Creation

Right click on the WeatherUseCase class name and generate a test. Once the test has been generated, annotate it with the RunWith() annotation and select the JUnitPlatform as the test runner. Once this is done, add a colon and the SubjectSpek<T>({}) wrapper to the class name. The T is the subject of the test. In our case, this would be the WeatherUseCase. Once this has all been done, your test should look something like this:

spek-3

Now that the code is set up, let’s investigate what’s actually going on:

  • The “@RunWith(JUnitPlatform::class)” code is telling JUnit to use the JUnitPlatform to run the tests instead of its own inbuilt test runner.
  • The “SubjectSpek<WeatherUseCase>({})” code is stating that the subject (or main focus) of the tests that will be carried out within the curly brackets is the WeatherUseCase.

The next thing to be done is to set up the subject of the test. In order to do this, we will utilise mocks. Create a mutable boolean flag (this will be used to determine if the device supports live updates) and an instance of the weatherService that will be initialised later on. Next create a subject and within the angled brackets of the subject, instantiate a mock of the weatherService. Finally create the WeatherUseCase and pass its dependencies that we just created.

spek-4

By doing this, we have just created the subject of our test and now within our tests, we will be able to access the weatherUseCase and its functions by simply calling subject.

The Group

As mentioned before, the Spek framework provides various functions for testing that are meant to give the developer more control over his/her tests. One of these functions is the group function. The group function is what is used by the Spek tests to collate (or group) similar tests into the same section (the importance of this will be explained later on).

The group function (which is the underlying function) is wrapped by three other functions.These functions are the given, describe and context functions. These functions are what the developer normally uses and are what we will use. If you go behind the scenes of each one of these functions, you’ll see that all they do is provide a different starting string to the description section of the group function.

spek-5

This was done so that the tester has more flexibility in regards to how they describe what’s happening in the test.

In our case, the two groups or scenarios that we will need to test will be how the WeatherUseCase behaves on a phone that supports live updates vs how it behaves on a phone that doesn’t support live updates.

spek-6

Please note that I could have used describe(“The app is …”) or context(“The app is …”) and functionally, it would have been exactly the same. I opted to use given(“…”) because, in this scenario, it reads better than describe or context.

The Action

Once we’ve set up our two scenarios, the next step is to specify the tests for both scenarios. The block that the Spek framework provides for actioning the test is the action block. Once again, Spek provides a wrapper function to help with the usage of the action block and that wrapper is the on block.

spek-7

The action block is, as its name suggests, responsible for executing the action that the test will later evaluate.

For our purposes, we want to verify two things. First of all, we want to ensure that in all scenarios, the weatherUseCase, when required, will request for the weekly weather forecast. Secondly, we want to verify that the weatherUseCase will only setup a live stream of weather updates when running on a device that supports live streams. These are the two things that we will test in both scenarios.

In order to test these cases, we will set up the appropriate on(“…”) blocks and will utilise the subject we created before to invoke the weatherUseCase functions that we want to test.

spek-8

The Test

The final block that is needed to complete the test is the it() block. The it() block evaluates the result of the action (i.e. it evaluates the results of whatever happened in the on() block). Thus we want to verify that:

(1) When running the app on a device that supports live updates:

  • It should, when requested, setup the live updates.
  • It should, when requested, get the weekly weather forecast.

(2) When running the app on a device that does not support live updates:

  • It should not setup live updates, even when requested.
  • It should, when requested, get the weekly weather forecast.

In order to confirm this, we will add an it() block within each on() block. The code within the it() block would specify the expected result for the test.

spek-9

If you were to run the tests at this point, one of the tests (setting up the live updates on a device that supports them) would fail. This is because the dependency that it relies on (the supportsLiveUpdates flag) is false (stating that it does not support live updates) instead of true.

The challenge faced here is that in two different scenarios, the same subject (the WeatherUseCase) is required to behave differently depending on its dependencies. We could change the supportsLiveUpdates flag to true just before running the tests that require it to be true but this would fail as well. This is because of the way the Spek tests run.

Though it may seem counterintuitive, Spek tests don’t run linearly, but rather run in two phases, discovery and execution. The group (given, describe and context) blocks run first in the discovery phase and then the on and it blocks run in the execution phase (credit). To demonstrate this, we’ll add some logging into each one of the blocks.

spek-10

Here are the logs from the test (Please ignore the large red error text and note the order of the logs that we put in):

spek-11

Here is an outline showing the order of execution:

spek-12

(I’ve removed the code and left the blocks in so that it is easier to see the order the blocks are being executed in).

So how would a developer get round this issue and properly test his/her code?

One potential way around this is to create two different WeatherUseCases: one for devices that support live updates and another for devices that don’t support weather updates. In some scenarios, this may be a valid option. For example, if the two devices running the app had radically different usages of the weather feature. If this were the case then two different use-cases would be justified. However, most of the time (and specifically in this case), you’ll have a specific set of functionality that for the most part, functions the same but in special scenarios, functions differently depending on various factors such as device-type, network accessibility, battery life, etc. When this happens, separate use-cases may not necessarily be justified. So, how then can one test the behaviour of a use-case (or anything else) that behaves differently under multiple scenarios?

This is where the power of Spek Tests shines through. Spek tests give the tester the ability to define per-group or per-test specific actions that can occur before and/or after each test or group is run. Consequently, this allows the tester to specify pre and post test (or group) actions and thus to carry out test state initialisation for specific groups of tests.

Before & After handlers

The blocks that Spek provides to do this are:

  • beforeGroup : This runs before each group
  • afterGroup: This runs after each group
  • beforeEachTest: This runs before each test
  • afterEachTest: This runs after each test

For our purposes, we’ll utilise the beforeGroup block to set up the dependencies needed for each group of tests.

spek-13

What this is doing is for each group of tests, it is setting up the dependencies needed by the subject for that group of tests before the it (the subject) is used in each on() and it() block.

If you run the test now, everything will pass and you’ll notice in the logs that all the tests are running in the correct order now.

spek-114

Conclusion

Spek/SubjectSpek testing can initially be a little hard to understand but once you get a grasp of it, it is a great tool to use and will significantly improve the quality of your code base. Hopefully this article has been helpful and kicks you off on your testing journey.

outcomes

How an incremental approach delivered big bang productivity gains for a major infrastructure business.

Transforming organisational performance / Leveraging emerging technology

    Copyright © Trimantium GrowthOps Limited. All Rights Reserved.