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

Unit Testing

What is a unit test?

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.

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

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.

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

Practically, a unit test is just a Kotlin class with annotated methods that tell the compiler how to treat the code. It’s best practice to have one test class for each implementation class that you want to test. e.g. class Main with 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 structure1.

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())
    }
}

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.

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

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


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