# Unit Testing

Unit testing is the practice of creating a correctness test for each aspect of a function. Unit tests rapidly reveal broken code, accelerating development speed.

There’s far more to testing than we can cover in this book, so this atom is only a basic introduction.

The “Unit” in “Unit testing” describes a small piece of code, usually a function, that is tested separately and independently. This should not be confused with the unrelated Kotlin Unit type.

Unit tests are typically written by the programmer, and run each time you build the project. Because unit tests run so frequently, they must run quickly.

You’ve been learning about unit testing while reading this book, via the AtomicTest library we use to validate the book’s code. AtomicTest uses the concise eq for the most common pattern in unit testing: comparing an expected result with a generated result.

Of the numerous unit test frameworks, JUnit is the most popular for Java. There are also frameworks created specifically for Kotlin. The Kotlin standard library includes kotlin.test, which provides a facade for different test libraries. This way you’re not limited to using a particular library. kotlin.test also contains wrappers for basic assertion functions.

To use kotlin.test, you must modify the dependencies section of your project’s build.gradle file to include:

testImplementation "org.jetbrains.kotlin:kotlin-test-common"

Inside a unit test, the programmer calls various assertion functions that validate the expected behavior of the function under test. Assertion functions include assertEquals(), which compares the actual value against an expected value, and assertTrue(), which tests its first argument, a Boolean expression. In this example, the unit tests are the functions with names beginning with the word test:

// UnitTesting/NoFramework.kt
package unittesting
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import atomictest.*

fun fortyTwo() = 42

fun testFortyTwo(n: Int = 42) {
  assertEquals(
    expected = n,
    actual = fortyTwo(),
    message = "Incorrect,")
}

fun allGood(b: Boolean = true) = b

fun testAllGood(b: Boolean = true) {
  assertTrue(allGood(b), "Not good")
}

fun main() {
  testFortyTwo()
  testAllGood()
  capture {
    testFortyTwo(43)
  } contains
    listOf("expected:", "<43>",
      "but was", "<42>")
  capture {
    testAllGood(false)
  } contains listOf("Error", "Not good")
}

In main(), you can see that a failing assertion function produces an AssertionError—this means the unit test has failed, signaling the problem to the programmer.

kotlin.test contains an assortment of functions that have names starting with assert:

  • assertEquals(), assertNotEquals()
  • assertTrue(), assertFalse()
  • assertNull(), assertNotNull()
  • assertFails(), assertFailsWith()

Similar functions are typically included in every unit test framework, but the names and parameter order can be different. For example, the message parameter in assertEquals() might be first or last. Also, it’s easy to mix up expected and actual—using named arguments avoids this problem.

The expect() function in kotlin.test runs a block of code and compares that result with the expected value:

fun <T> expect(
  expected: T,
  message: String?,
  block: () -> T
) {
  assertEquals(expected, block(), message)
}

Here’s testFortyTwo() rewritten using expect():

// UnitTesting/UsingExpect.kt
package unittesting
import atomictest.*
import kotlin.test.*

fun testFortyTwo2(n: Int = 42) {
  expect(n, "Incorrect,") { fortyTwo() }
}

fun main() {
  testFortyTwo2()
  capture {
    testFortyTwo2(43)
  } contains
    listOf("expected:",
      "<43> but was:", "<42>")
  assertFails { testFortyTwo2(43) }
  capture {
    assertFails { testFortyTwo2() }
  } contains
    listOf("Expected an exception",
      "to be thrown",
      "but was completed successfully.")
  assertFailsWith<AssertionError> {
    testFortyTwo2(43)
  }
  capture {
    assertFailsWith<AssertionError> {
      testFortyTwo2()
    }
  } contains
    listOf("Expected an exception",
      "to be thrown",
      "but was completed successfully.")
}

It’s important to add tests for corner cases. If a function produces an error under certain conditions, this should be verified with a unit test (as AtomicTest’s capture() does). assertFails() and assertFailsWith() ensure that the exception is thrown. assertFailsWith() also checks the type of the exception.

# Test Frameworks

A typical test framework contains a collection of assertion functions and a mechanism to run tests and display results. Most test runners show results with green for success and red for failure.

This atom uses JUnit5 as the underlying library for kotlin.test. To include it in a project, the dependencies section of your build.gradle should look like this:

testImplementation "org.jetbrains.kotlin:kotlin-test"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit5"
testImplementation "org.junit.jupiter:junit-jupiter:$junit_version"

If you’re using a different library, you can find setup details in that framework’s instructions.

kotlin.test provides facades for the most commonly used functions. Assertions are delegated to the appropriate functions in the underlying test framework. In the org.junit.jupiter.api.Assertions class, for example, assertEquals() calls Assertions.assertEquals().

Kotlin supports annotations for definitions and expressions. An annotation is the @ sign followed by the annotation name, and indicates special treatment for the annotated element. The @Test annotation converts a regular function into a test function. We can test fortyTwo() and allGood() using the @Test annotation:

// Tests/unittesting/SampleTest.kt
package unittesting
import kotlin.test.*

class SampleTest {
  @Test
  fun testFortyTwo() {
    expect(42, "Incorrect,") { fortyTwo() }
  }
  @Test
  fun testAllGood() {
    assertTrue(allGood(), "Not good")
  }
}

kotlin.test uses a typealias to create a facade for the @Test annotation:

typealias Test = org.junit.jupiter.api.Test

This tells the compiler to substitute the @org.junit.jupiter.api.Test annotation for @Test.

A test class usually contains multiple unit tests. Ideally, each unit test only verifies a single behavior. This quickly guides you to the problem if a test fails when introducing new functionality.

@Test functions can be run:

  • Independently
  • As part of a class
  • Together with all tests defined for the application

IntelliJ IDEA allows you to rerun only the failed tests.

Consider a simple state machine with three states: On, Off and Paused. The functions start(), pause(), resume() and finish() control the state machine. resume() is valuable because resuming a paused machine is significantly cheaper and/or faster than starting a machine.

// UnitTesting/StateMachine.kt
package unittesting
import unittesting.State.*

enum class State { On, Off, Paused }

class StateMachine {
  var state: State = Off
    private set
  private fun transition(
    new: State, current: State = On
  ) {
    if(new == Off && state != Off)
      state = Off
    else if(state == current)
      state = new
  }
  fun start() = transition(On, Off)
  fun pause() = transition(Paused, On)
  fun resume() = transition(On, Paused)
  fun finish() = transition(Off)
}

These operations are ignored:

  • resume() or finish() on a machine that is Off.
  • pause() or start() on a Paused machine.

To test StateMachine, we create a property sm inside the test class. The test runner creates a fresh StateMachineTest object for each different test:

// Tests/unittesting/StateMachineTest.kt
package unittesting
import kotlin.test.*

class StateMachineTest {
  val sm = StateMachine()
  @Test
  fun start() {
    sm.start()
    assertEquals(State.On, sm.state)
  }
  @Test
  fun `pause and resume`() {
    sm.start()
    sm.pause()
    assertEquals(State.Paused, sm.state)
    sm.resume()
    assertEquals(State.On, sm.state)
    sm.pause()
    assertEquals(State.Paused, sm.state)
  }
  // ...
}

Normally, Kotlin only allows letters and digits for function names. However, if you put a function name inside backticks, you can use any characters (including whitespaces). This means you can create function names that are sentences describing their tests, such as pause and resume. This produces more useful error information.

An essential goal of unit testing is to simplify the gradual development of complicated software. After introducing each new piece of functionality, a developer not only adds new tests to check its correctness but also runs all the existing tests to make sure that the prior functionality still works. You feel safer when introducing new changes, and the system is more predictable and stable.

In the process of fixing a new bug, you create additional unit tests for this and similar cases, so you don’t make the same mistakes in the future.

If you use a continuous integration (CI) server such as Teamcity (opens new window), all available tests run automatically and you’re notified if something breaks.

Consider a class with several properties:

// UnitTesting/Learner.kt
package unittesting

enum class Language {
  Kotlin, Java, Go, Python, Rust, Scala
}

data class Learner(
  val id: Int,
  val name: String,
  val surname: String,
  val language: Language
)

It’s often helpful to add utility functions for manufacturing test data, especially when you must create many objects with the same default values during testing. Here, makeLearner() creates objects with default values:

// Tests/unittesting/LearnerTest.kt
package unittesting
import unittesting.Language.*
import kotlin.test.*

fun makeLearner(
  id: Int,
  language: Language = Kotlin,         // [1]
  name: String = "Test Name $id",
  surname: String = "Test Surname $id"
) = Learner(id, name, surname, language)

class LearnerTest {
  @Test
  fun `single Learner`() {
    val learner = makeLearner(10, Java)
    assertEquals("Test Name 10", learner.name)
  }
  @Test
  fun `multiple Learners`() {
    val learners = (1..9).map(::makeLearner)
    assertTrue(
      learners.all { it.language == Kotlin })
  }
}

Adding default arguments to Learner that are only for testing introduces unnecessary complexity and potential confusion. makeLearner() is easier and cleaner when producing test instances, and it eliminates redundant code.

The order of makeLearner()’s parameters simplifies its usage. In this case, we expect to specify a non-default lang more often than changing default test values for name and surname, so the lang parameter is second ([1]).

# Mocking and Integration Tests

A system that depends on other components complicates the creation of isolated tests. Rather than introducing dependencies on real components, programmers often use a practice called mocking.

A mock replaces a real entity with a fake one during testing. Databases are commonly mocked to preserve the integrity of the stored data. The mock can implement the same interface as the real one, or it can be created using mocking libraries such as MockK (opens new window).

It’s vital to test separate pieces of functionality independently—that’s what unit tests do. It’s also essential to ensure that different parts of the system work when combined with each other—that’s what integration tests do. Unit tests are “inward-directed” while integration tests are “outward-directed”.

# Testing Inside IntelliJ IDEA

IntelliJ IDEA and Android Studio support creating and running unit tests.

To create a test, right-click (control-click on a Mac) the class or function you want to test and select “Generate…” from the pop-up menu. From the “Generate” menu, choose “Test…”. A second approach is to open the list of “intention actions” (opens new window), and select “Create Test” (opens new window).

Select JUnit5 as the “Testing library”. If a message appears saying “JUnit5 library not found in the module,” push the “Fix” button next to the message. The “Destination package” should be unittesting. The result will end up in another directory (always separate tests from main code). The Gradle default is the src/test/kotlin folder, but you can choose a different destination.

Check the boxes next to the functions you want tested. You can automatically navigate from the source code to the corresponding test class and back; for details see the documentation (opens new window).

Once the test framework code is generated, you can modify it to suit your needs. For the examples and exercises in this atom, replace:

import org.junit.Test
import org.junit.Assert.*

with:

import kotlin.test.*

When running tests within IntelliJ IDEA, you may get an error message like “test events were not received.” This is because IDEA’s default configuration assumes you are running your tests externally, using Gradle. To fix it so you can run your tests inside IDEA, start at the file menu:

File | Settings | Build, Execution, Deployment | Build Tools | Gradle

On that page you’ll see a drop-down titled “Run tests using:” which is set to “Gradle (Default)”. Change this to “IntelliJ IDEA” and your tests will run correctly.

Exercises and solutions can be found at www.AtomicKotlin.com.