13 minutes read

In this topic, you will learn about automated unit testing in Kotlin and write your own unit tests. As you probably know, a unit is a piece of code that performs a single task or a unit of work. In most cases, it conforms to a class. You can write and execute tests to check if the methods of that class work as expected.

Note that we use unit tests to test our program without its external dependencies such as databases, web services, and so on, which would fall into the category of integration tests.

In Kotlin, methods may return values or change the internal state of objects. To verify the correctness of any method, you may compare the value returned by that method with the expected value or compare the internal state of an object modified by that method with the expected internal state. Of course, you can do the tests manually, but it tends to be tedious and time-consuming. That's why some frameworks were developed to provide convenient tools for automated unit testing. The most popular of them is JUnit.

Getting started

Let's create a simple calculator that performs basic calculations on integers and checks for even numbers:

object Calculator {
    
    fun add(a: Int, b: Int) = a + b
    
    fun subtract(a: Int, b: Int) = a - b

    fun multiply(a: Int, b: Int) = a * b

    fun divide(a: Int, b: Int) : Int {
        if (b == 0) throw IllegalArgumentException("Divisor cannot be zero!")
        return a / b
    }

    fun isEven(a: Int) = (a % 2) == 0
}

Our object has five methods: add, subtract, multiply, divide, and isEven, which we are going to test.

In this topic, we will use JUnit 5 since it is the most recent version of the JUnit framework. JUnit 5 requires Java 8 (or higher) at runtime but can also be used to test code compiled with previous versions of the JDK.

To start working with JUnit 5, you need to add the required dependencies to your project. If you use Gradle as your project build tool, add the following dependency to the build.gradle file:

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
}

Currently, 5.9.2 is the most recent version of the framework. You can always check Maven central for the most current version of the framework.

Also, to correctly use Gradle with JUnit 5, add the code below to the top level of the build.gradle file:

test {
    useJUnitPlatform()
}

This tells Gradle to use JUnitPlatform to run the tests, otherwise, Gradle will not be able to see your tests and run them. Finally, load the Gradle changes.

Writing tests

Now you are ready to write your first test. Create a class in your project's src/test/kotlin folder and name it CalculatorTest. Test classes are named beginning with the name of the class or object they are testing, and ending with "Test" Alternatively, if you are using IntelliJ IDEA, right-click on the class or object name and select Generate... (Alt + Insert), and then Test... in the drop-down menu. A pop-up window will appear. Leave the defaults and click OK. The IDE will then create the test class for you.

Inside the class, add a new method testAddition and annotate it with @Test from org.junit.jupiter.api.Test. This annotation tells the JUnit framework that the method is a unit test method.

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals

class CalculatorTest {
    
    @Test
    fun `when adding 1 and 2 expect 3`() {
        val result = Calculator.add(1, 2)
        assertEquals(3, result)
    }
}

Test functions should have clear and descriptive names that make it easy to understand the purpose of the test and identify the source of a failure or bug when running multiple tests. A common naming convention is to use the format FunctionName_TestCondition_ExpectedOutcome. This helps to clearly communicate the purpose of the test, including the method being tested, the inputs or conditions that the method is being called with, and the expected outcome. For example, add_addingOneAndTwo_shouldReturnThree. Another common naming convention is When_TestCondition_Expect_ExpectedOutcome which will be used in this topic. Note that there are other naming conventions for test functions. In kotlin, we can write function names with spaces as in the example above using ``. This improves readability and allows for more human-like naming of tests. It's important to keep the naming consistent across the project so that test cases are easy to understand and identify.

It's worth noting that you can also use the @DisplayName("A description of the test") annotation to describe your tests while keeping the function name simple. This is rarely used but can be useful in certain cases.

Inside our test method, we used the add method from our Calculator which is supposed to return the sum of 1 and 2 provided as arguments. The returned value is stored in the result variable. After that, we check if the expected result is the same as the actual returned value stored in result using the assertEquals method. It compares the expected value (in this case, the sum of 1 and 2 is obviously 3, given as the first argument) with the actual value returned by the method we are testing (given as the second argument) and throws AssertionFailedError if they are not equal.

To run the test in IntelliJ IDEA, click on the green icon in the gutter beside the test method then click the Run button or right-click on the test file in the project view panel and select Run CalculatorTest in the drop-down menu. The Run panel opens showing the progress and the outcome of the test. In our case, the test should pass:

test run example in IntelliJ IDEA

We'll discuss test running more in this topic. Now go ahead and add other tests for the remaining methods of our Calculator.

Assertions

The Assertions class of the JUnit framework has a lot of overloaded methods that will allow you to test different conditions. In our previous example, we only tested for equality between integers but the same method can be used to test on values of other data types such as strings.

Here are some useful assertions:

assertEquals

tests if the arguments are equal

assertTrue

tests if a value is true

assertFalse

tests if the argument is false

assertNull

tests if the argument is null

assertNotNull

tests if the argument is not null

assertThrows

tests if the argument throws a certain exception

All of them have overloads that accept a message of the String type which is displayed if the test fails. You may find detailed descriptions of these and other assertions in the official JUnit 5 documentation.

Assuming you added the tests for the other methods of our Calculator, you probably used the method assertEquals for all the tests. At this point, you could change the test for isEven method to use the assertTrue assertion:

...
import org.junit.jupiter.api.assertTrue

class CalculatorTest {
    
    ...

    @Test
    fun `when 2 is checked if even expect true`() {
        assertTrue(Calculator.isEven(2))
    }
}

We could also add a test to check if our division method throws an IllegalArgumentException if the second argument provided when calling it is 0:

...
import org.junit.jupiter.api.assertThrows

class CalculatorTest {

    ...
    
    @Test
    fun `when dividing by 0 expect IllegalArgumentException`() {
        assertThrows<IllegalArgumentException> {
            Calculator.divide(10, 0)
        }
    }
}

Note that this type of exception is passed in angle brackets and the code that throws the exception is placed in as a trailing lambda.

Running tests

Now we have a few unit tests which we may run. You can run individual tests by clicking the green icon beside the specific test or run all the tests by clicking the green icon beside the test class.

Another way to run the tests is by using the terminal window. First, add the following lines to the build.gradle file to see a more detailed output for the executed tests:

test {
    ...
    
    testLogging {
        events "passed", "skipped", "failed"
    } 
}

After that, run the following command in the Terminal window (make sure you are in your project's root directory):

./gradlew test

Once the tests are executed, you will get this output:

...
> Task :test
CalculatorTest > when adding 1 and 2 expect 3() PASSED
CalculatorTest > when subtracting 2 from 3 expect 1() PASSED
CalculatorTest > when multiplying 2 by 3 expect 6() PASSED
CalculatorTest > when dividing 4 by 2 expect 2() PASSED
CalculatorTest > when 2 is checked if even expect true() PASSED
CalculatorTest > when dividing by 0 expect IllegalArgumentException() PASSED

BUILD SUCCESSFUL in 3s

The output shows the task that has been executed, the names of all tests, and the status of their execution.

Note that if a test method has an empty body it will be counted as PASSED. If you want to force your test to fail, you have to invoke the fail method inside it.

Test outcomes

So far, all our tests have passed. This proves that our Calculator functions correctly.

Let's introduce a bug in our code. Change the add method for our Calculator so that it always returns 0:

object Calculator {
    
    fun add(a: Int, b: Int) = 0
    ...
}

Run the tests again, using the Terminal:

./gradlew test

The addition test fails as expected:

...
> Task :test
CalculatorTest > when adding 1 and 2 expect 3() FAILED
    org.opentest4j.AssertionFailedError at CalculatorTest.kt:15
CalculatorTest > when subtracting 2 from 3 expect 1() PASSED
CalculatorTest > when multiplying 2 by 3 expect 6() PASSED
CalculatorTest > when dividing 4 by 2 expect 2() PASSED
CalculatorTest > when 2 is checked if even expect true() PASSED
CalculatorTest > when dividing by 0 expect IllegalArgumentException() PASSED

5 tests completed, 1 failed

The output shows that when adding 1 and 2 expect 3 failed with an AssertionFailedError at line 15 in CalculatorTest.kt.

Sometimes you may want to skip a test that was implemented. Maybe because it's testing a feature that is not yet ready for release or the feature contains bugs that are not yet fixed. To do so, annotate the test method with @Disabled:

...

class CalculatorTest {
    
    @Disabled
    @Test
    fun `when adding 1 and 2 expect 3`() {
        ...
    }
}

Run the tests again, and you will get a similar output:

...
> Task :test
CalculatorTest > when adding 1 and 2 expect 3() SKIPPED
CalculatorTest > when subtracting 2 from 3 expect 1() PASSED
CalculatorTest > when multiplying 2 by 3 expect 6() PASSED
CalculatorTest > when dividing 4 by 2 expect 2() PASSED
CalculatorTest > when 2 is checked if even expect true() PASSED
CalculatorTest > when dividing by 0 expect IllegalArgumentException() PASSED

BUILD SUCCESSFUL in 3s

The test annotated with @Disabled is skipped resulting in a successful build.

Now, fix the faulty method of our Calculator. Remove the @Disabled annotation from the addition test and then run the tests again — all of them should pass. Excellent! The bug is gone.

Conclusion

JUnit framework provides API for unit testing Kotlin classes. With its help, you can set up and run automated tests to check the results of the execution of your units of code against desired criteria. You may run tests from your IDE or use project build tools such as Gradle. If a test fails, JUnit will show a detailed output to help you understand the reasons for it.

You write tests using assertions, the most frequently used of which is the assertEquals method. Running a test can result in either SUCCESS if everything works as expected or FAILURE if the assertion method throws AssertionFailedError.

82 learners liked this piece of theory. 6 didn't like it. What about you?
Report a typo