TDD & Unit Testing

Test-Driven Development (TDD) is a strategy introduced by Kent Beck, which suggests writing tests first, before you start coding. You write tests against expected behaviour, and then write code which works without breaking the tests. TDD suggests this process:

  1. Think about the function (or class) that you need to create.
  2. Write tests that describe the behaviour of that function or class. As above, start with valid and invalid input.
  3. Your test will fail (since the implementation code doesn’t exist yet). Write just enough code to make the tests pass.
  4. Repeat until you can’t think of any more tests to write.

Running out of tests means that there is no more behaviour to implement… and you’re done, with the benefit of fully testable code.

Red, Green, Refactor - Red, Green, Refactor -

Courtesy of https://medium.com/@tunkhine126/red-green-refactor-42b5b643b506

Info

The IntelliJ IDEA docs have a section on TDD using Kotlin.

Why TDD?

There are some clear benefits:

  • Early bug detection. You are building up a set of tests that can verify that your code works as expected.
  • Better designs. Making your code testable often means improving your interfaces, having clean separation of concerns, and cohesive classes. Testable code is by necessity better code.
  • Confidence to refactor. You can make changes to your code and be confident that the tests will tell you if you have made a mistake.
  • Simplicity. Code that is built up over time this way tends to be simpler to maintain and modify.

What are unit tests?

Let’s define it more formally. A unit test is a test that meets the following three requirements [Khorikov 2020]:

  • Verifies a single unit of behavior,
  • Does it quickly, and
  • Does it in isolation from other tests.

Unit tests should target classes or components in your program. i.e. they should exercise how a particular class works. They should be small, very focused, and quick to execute and return results.

Testing in isolation means removing the effects of any dependencies on other code, libraries or systems. This means that when you test the behaviour of your class, you are assured that nothing else is contributing to the results that you are observing. We’ll discuss strategies to accomplish this in the [Dependencies] section.

Info

This is also why designing cohesive, loosely coupled classes is critical: it makes testing them so much easier if they work independently!

Unit tests are just Kotlin functions that execute and check the results returned from other functions. Ideally, you would produce one unit or more unit tests for each function. You would then have a set of unit tests to check all of the methods in a class, and multiple sets of units tests to cover all of your implementation classes.

Your goal should be to have unit tests for every critical class and function that you produce1.

Installing JUnit

We’re going to use JUnit, a popular testing framework2 to create and execute our unit tests. It can be installed in number of ways: directly from the JUnit home page, or one of the many package managers for your platform.

We will rely on Gradle to install it for us as project dependency. If you look at the section of a build.gradle file below, you can see that JUnit is included, which means that Gradle will download, install and run it as required. IntelliJ projects will typically include Gradle by default.

dependencies {
  // Use the Kotlin test library.
  testImplementation org.jetbrains.kotlin:kotlin-test'

  // Use the Kotlin JUnit integration.
  testImplementation'org.jetbrains.kotlin:kotlin-test-junit'
}

Structuring a test

A typical unit test uses a very particular pattern, known as the arrange, act, assert pattern. This suggests that each unit test should consist of these three parts:

  1. Arrange: bring the system under test (SUT) to the starting state.
  2. Act: call the method or methods that you want to test.
  3. Assert: verify the outcome of the above action. This can be based on return values, or some other conditions that you can check.

Note that your Act section should have a single action, reflecting that it’s testing a single behaviour. If you have multiple actions taking place, that’s a sign that this is probably an integration test (see below). As much as possible, you want to ensure that you are writing minimal-scope unit tests.

Info

Another anti-pattern is an if statement in a test. If you are branching, it means that you are testing multi-ple things, and you should really consider breaking that one test into multiple tests instead.

How to write tests

A unit test is just a Kotlin class, with annotated methods that tell the compiler to treat the code as a test. It’s best practice to have one test class for each implementation class that you want to test. e.g. class Main has a test class MainTest. This test class would contain multiple methods, each representing a single unit test.

Tests should be placed in a special test folder in the Gradle directory structure. When building, Gradle will automatically execute any tests that are placed in this directory structure3.

Gradle Unit Tests Gradle Unit Tests

Your unit tests should be structured to create instances of the classes that they want to test, and check the results of each function to confirm that they meet expected behaviour.

For example, here’s a unit test that checks that the Model class can add an Observer properly. The addObserver() method is the test (you can tell because it’s annotated with @Test). The @Before method runs before the test, to setup the environment. We could also have an @After method if needed to tear down any test structures.

class ObserverTests() {
    lateinit var model: Model
    lateinit var view: IView

    class MockView() : IView {
        override fun update() {
        }
    }

    @Before
    fun setup() {
        model = Model()
        view = MockView()
    }

    @Test
    fun addObserver() {
        val old = model.observers.count()
        model.addObserver(view)
        assertEquals(old+1, model.observers.count())
    }

Gradle will automatically execute and display the test results when you build your project.

Info

Creating unit tests is covered more fully in the Testing & Evaluation chapter!

Let’s walkthrough creating a test.

To do this in IntelliJ, select a class in the editor, press Alt-Enter, and select “Create Test” from the popup menu. This will create a unit test using JUnit, the default test framwork. There is a detailed walkthrough on the IntelliJ support site.

Info

You can also just create the test files by-hand. Just make sure to save them in the correct location!

Create a unit test Create a unit test

The convention is to name unit tests after their class they’re testing, with “Test” added as a suffix. In this example, we’re creating a test for a Model class so the test is automatically named ModelTest. This is just a convention - you can name it anything that you want.

We’ve manually added the addition() function. We can add as many functions as we want within this class. By convention, they should do something useful with that particular class.

Below, class ModelTest serves as container for our test functions. In it, we have two unit tests that will be automatically executed when we built. NOTE that you would need more than these two tests to adequately test this class - this is just an example.

import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class ModelTests {
    lateinit var model: Model
    
    @Before
    fun setup() {
        model = Model()
        model.counter = 10
    }

    @Test
    fun checkAddition() {
        val original = model.counter
        model.counter++
        assertEquals(original+1, model.counter)
    }

    @Test
    fun checkSubtraction() {
        val original = model.counter
        model.counter--
        assertEquals(original-1, model.counter)
    }

    @After
    fun teardown() {
    }
}

The kotlin.test package provides annotations to mark test functions, and denote how they are managed:

Annotation Purpose
@Test Marks a function as a test to be executed
@BeforeTest Marks a function to be invoked before each test
@AfterTest Marks a function to be invoked after each test
@Ignore Mark a function to be ignored
@Test Marks a function as a test

In our test, we call utility functions to perform assertions of how the function should successfully perform.

Function Purpose
assertEquals Provided value matches the actual value
assertNotEquals The provided and actual values do not match
assertFalse The given block returns false
assertTrue The given block returns true

How to run tests

Tests will be run automatically with gradle build or we can execute gradle test to just execute the tests.

$ gradle test
BUILD SUCCESSFUL in 760ms
3 actionable tasks: 3 up-to-date

Gradle will report the test results, including which tests - if any - have failed.

You can also click on the arrow beside the test class or name in the editor. For example, clicking on the arrow in the gutter would run this addObserver() test.

Simple unit test Simple unit test

Code coverage

Tests shouldn’t verify units of code. Rather, they should verify units of behaviour: something that is meaningful for the problem domain and, ideally, something that a business person can recognize as useful. The number of classes it takes to implement such a unit of behaviour is irrelevant. — Khorikov (2020)

Code coverage is a metric comparing the number of lines of code with the number of lines of code that have unit tests covering their execution. In other words, what “percentage” of your code is tested?

This is a misleading statistic at the best of times (we can easily contrive cases where code will never be executed or tested).

TDD would suggest that you should have 100% unit test coverage but this is impractical and not that valuable. You should focus instead on covering key functionality. e.g. domain objects, critical paths of your source code.

One recommendation is to look at the coverage tools in IntelliJ, which will tell you how your code is being executed, as well as what parts of your code are covered by unit tests. Use this to determine which parts of the code should be tested more completely.

Code Coverage Code Coverage

https://www.jetbrains.com/help/idea/code-coverage.html

https://www.jetbrains.com/help/idea/running-test-with-coverage.html

Unreachable code Unreachable code

https://xkcd.com/2200/


  1. You will NOT likely get 100% code coverage - see the Testing chapter on dependencies and limits to what we can realistically test. ↩︎

  2. Kent Beck and Eric Gamma invented xUnit, a Smalltalk unit testing framework, while on a flight to OOPSLA in 1997. Over time, it was adapted into nUnit for .NET, CPPUnit for C++ and JUnit for Java. ↩︎

  3. Android has two test folders: src/test is used for tests that can run on your development machine, and src/androidTest is used for tests that need to run on an Android device (e.g. the AVD where you are testing). ↩︎