CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

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 -

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

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?

This is covered more fully in the Testing & Evaluation chapter.

A unit test is a test that meets the following three requirements [Khorikov 2020]:

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

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'
}

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

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.

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

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

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

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

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). ↩︎