Note this blog post contains my notes from the Preventing Failure section of the book Atomic Kotlin. Consider buying a copy of the book for a more detailed reference.
Exception Handling
Exceptions conflate three activities
- Error Reporting
- Recovery
- Resource Cleanup
You can inherit new exception types from Exception or a subtype:
package exceptionhandling
import atomictest.*
class Exception1(
val value: Int
): Exception("wrong value: $value")
open class Exception2(
description: String
): Exception(description)
class Exception3(
description: String
): Exception2(description)
fun main() {
capture {
throw Exception(13)
} eq "Exception1: wrong value: 13"
capture {
throw Exception3("error")
} eq "Exception3: error"
}
A throw expression as in the above example requires an instance of a Throwable subtype.
If an error is not caught then it produces a stack trace:
package stcktrace
import exceptionhandling.Exception1
fun function1(): Int =
throw Exception1(-52)
fun function2() = function1()
fun function3() = function2()
fun main() {
//function3() causes a stacktrace
}
Example with exception handlers:
package exceptionhandling
import atomictest.eq
fun toss(which: Int) =
when(this) {
1 -> throw Exception(1)
2 -> throw Exception2("Exception 2")
3 -> throw Exception3("Exception 3")
else -> "OK"
}
fun test(which: Int): Any? =
try {
toss(which)
} catch (e: Exception1) {
e.value
} catch (e: Exception3) {
e.message
} catch (e: Exception2) {
e.message
}
fun main() {
test(0) eq "OK"
test(1) eq 1
test(2) eq "Exception 2"
test(3) eq "Exception 3"
}
try-catch above is a single expression. Try-catch returns the last expression of the try body or the last expression of the catch clause matching an exception
If no catch handles the exception, that exception is thrown further up the stack.
Because Exception3 extends Exception2, an Exception3 is handled as an Exception2, if Exception2’s catch appears before Exception3’s catch.
Another Example:
package exceptionhandling
import atomictest.*
fun testCode(code: Int) {
if (code <= 1000) {
throw IllegalArgumentException(
"'code' must be > 1000: $code"
)
}
}
fun main() {
try {
testCode("A16".toInt(16))
} catch (e: IllegalArgumentException) {
e.message eq
"'code' must be > 1000: 161"
}
try {
testCode("0".toInt(1))
} catch(e: IllegalArgumentException) {
e.message eq
"radix 1 was not in a valid range 2..36"
}
}
The above example is confusing because it catches the same type of Exception for both cases.
Some cleaner code:
package exceptionhandling
import atomictest.eq
class IncorrectInputException(
message: String
): Exception(message)
fun checkCode(code: Int) {
if (code <= 1000) {
throw IncorrectInputException(
"Code must be > 1000: $code"
)
}
}
fun main() {
try {
checkCode("A1".toInt(16))
} catch (e: IncorrectInputException) {
e.message eq "Code must be > 1000: 161"
} catch (e: IllegalArgumentException) {
"Produces error" eq "if it gets here"
}
try {
checkCode("1".toInt(1))
} catch (e: IncorrectInputException) {
"Produces error" eq "if it gets here"
} catch (e: IllegalArgumentException) {
e.message eq
"radix 1 was not in valid range 2..36"
}
}
Below example shows how to use finally for resource cleanup. A finally clause always runs regardless of whether you catch an error or run the try block:
package exceptionhandling
import atomictest.*
fun checkValue(value: Int) {
try {
trace(value)
if (value <= 0)
throw IllegalArgumentException(
"value must be positive: $value"
)
} finally {
trace("In finally clause for $value")
}
}
fun main() {
listOf(10, -10).forEach {
try {
checkValue(it)
} catch (e: IllegalArgumentException) {
trace("In catch clause for main()")
trace(e.message)
}
}
trace eq """
10
In finally clause for 10
-10
In finally clause for -10
In catch clause for main()
value must be positive: -10
}
Example how capture() in atomictest is implemented:
package exceptionhandling
import atomictest.CaptureException
fun capture(f: () -> Unit): CapturedException =
try {
f()
CapturedException(null,
"<Error>: Expected an exception"
)
} catch (e: Throwable) {
CapturedException(e::class,
if (e.message != null) ": ${e.message}"
else
""
)
}
fun main() {
capture {
throw Exception("!!!")
} eq "Exception: !!!"
capture {
1
} eq "<Error>: Expected an exception"
}
The primary purpose of exceptions in Kotlin is to discover program bugs and not recovery.
Check Instructions
Check instructions assert that constraints are satisfied. They are commonly used to validate function args and results
Example of a precondition using require:
package checkinstructions
import atomictest.*
data class Month(val monthNumber: Int) {
init {
require(monthNumber in 1..12) {
"Month out of range: $monthNumber
}
}
}
fun main() {
Month(1) eq "Month(monthNumber=1)"
capture { Month(13) } eq
"IllegalArgumentException: " +
"Month out of range: 13"
}
You can always use require instead of throwing an IllegalArgumentException
Another Example:
package checkinstructions
import kkotlin.math.sqrt
import atomictest.*
class Roots(
val root1: Double
val root2: Double
)
fun quadraticZeroes(
a: Double,
b: Double,
c: Double
): Roots {
require(A != 0.0) { "a is zero" }
val underRadical = b * b - 4 * a * c
require(underRadical >=0 {
"Negative underRadical: $underRadical"
}
val squareRoot = sqrt(underRadical)
val root1 = (-b - squareRoot) / (2 * a)
val root1 = (-b + squareRoot) / (2 * a)
return Roots(root1, root2)
}
fun main() {
capture {
quadraticZeroes(0.0, 4.0, 5.0)
} eq "IllegalArgumentException: " +
"a is zero"
capture {
quadraticZeroes(3.0, 4.0, 5.0)
} eq "IllegalArgumentException: " +
"Negative underRadical: -44.0"
val roots = quadraticZeroes(1.0, 2.0, -8.0)
roots.root1 eq -4.0
roots.root2 eq 2.0
}
Defining DataFile class:
package checkinstructions
import atomictest.eq
import java.io.file
import java.nio.file.Paths
val targetDir= File("DataFiles")
class DataFile(val fileName: String):
Flie(targetDir, fileName) {
init {
if (!targetDir.exists())
targetDir.mkdir()
}
fun erase() { if (exists()) delete() }
fun reset: File {
erase()
createNewFile()
return this
}
}
fun main9) {
DataFile("Test.txt").reset() eq
Paths.get("DataFiles", "Test.txt")
.toString()
}
We could then check things about the file using require():
package checkinstructions
import atomictest.*
fun getTrace(fileName: String): List<String> {
require(fileName.startsWith("file_")) {
"$fileName must start with 'file_'"
}
val file = DataFile(fileName)
require(file.exists()) {
"$fileName doesn't exist"
}
val lines = file.readLines()
require(lines.isNotEmpty()) {
"$fileName is empty"
}
return lines
}
fun main() {
DataFile("file_empty.txt").writeText("")
DataFile("file_wubba.txt").writeText(
"wubba lubba dub dub"
)
capture {
getTrace("wrong_name.txt")
} eq "IllegalArgumentException: " +
"wrong_name.txt must start with file_"
capture {
getTrace("file_nonexistent.txt")
} eq "IllegalArgumentException: " +
"file_nonexistent.txt doesn't exist"
capture {
getTrace("file_empty.txt")
} eq "IllegalArgumentException: " +
"file_empty.txt is empty"
getTrace("file_wubba.txt") eq
"[wubba lubba dub dub]"
}
require() without the lambda produces a default message of “Failed requirement.”
requireNotNull() tests its first arg and returns that arg if its not null. Otherwise it produces an IllegalArgumentException. Upon success requireNotNull() is auto smart-cast to a non-nullable type so you don’t need it’s return value.
check() is identical to require() except that it throws IllegalStateException and is typically used at the end of a function
Example:
package checkinstructions
import atomictest.*
val resultFile = DataFile("Results.txt")
fun createResultFile(create: Boolean) {
if (create)
resultFile.writeText("Results\n* ok"
check(resultFile.exists()) {
"${resultFile.name} doesn't exist"
}
}
fun main() {
resultFile.erase()
capture {
createResultFile(false)
} eq "IllegalStateException: " +
"Results.txt doesn't exist!"
createResultFile(true)
}
assert() allows you to enable or disable assert() checks
You must use the Kotlin flag -ea if you decide to use assert()
The Nothing Type
A Nothing return type indicates a function that never returns
This is usually a function that always throws an exception
Example defining an infinite loop:
package nothing type
fun infinite(): Nothing {
while (true) { }
}
Nothing is a built-in type with no instances.
TODO() has a return type of Nothing and throws NotImplementedError:
package nothingtype
import atomictest.*
fun later(s: String): String = TODO("later()")
fun later2(s: String): Int = TODO()
fun main() {
capture {
later("Hello")
} eq "NotImplementedError: " +
"An operation is not implemented: later()"
capture {
later2("Hello!")
} eq "NotImplementedError: " +
"An operation is not implemented."
}
Nothing is compatible with any type like the return types of String and Int in the above example.
More readable and compact way of always throwing an exception:
package nothingtype
import atomictest.*
fun fail(i: Int): Nothing =
throw Exception("fail($i)")
fun main() {
capture {
fail(1)
} eq "Exception: fail(1)"
capture {
fail(2)
} eq "Exception: fail(2)"
}
Another Example:
package nothingtype
import atomictest.*
class BadData(m: String): Exception(m)
fun checkObject(obj: Any?): String =
if (obj is String)
obj
else
throw BadData("Needs String, got $obj")
fun test(checkObj: (obj: Any?) -> String) {
checkObj("abc") eq "Abc"
capture {
checkObj(null)
} eq "BadData: Needs String, got null"
capture {
checkObj(123)
} eq "BadData: Needs String, got 123"
}
fun main() {
test(::checkObj)
}
Kotlin treats a throw as type Nothing
Rewriting the above example:
package nothingtype
fun failWithBadData(obj: Any?): Nothing =
throw BadData("Needs String: got $obj)
fun checkObject2(obj: Any?): String =
(obj as? String) ?: failWithBadData(obj)
fun main() {
test(::checkObject2)
}
When given a plain null with no additional type information the compiler infers a type nullable Nothing?
import atomictest.eq
fun main() {
val none: Nothing? = null
val nullableString: String? = null
nullableString = "abc"
nullableString = none
nullableString eq null
val nullableInt: Int? = none
nullableInt eq null
val listNone: List<Nothing?> = listOf(null)
val ints: List<Int?> = listOf(null)
ints eq listNone
}
You can assign both null and none to a nullable type because the types of null and none are Nothing?
Resource Cleanup
use() guarantees proper cleanup of closeable resources.
use() works with any object that implements Java’s AutoCloseable interface.
It executes code within the block then calls close() on the object regardless of how you exit the block.
use() rethrows all exceptions so you must still deal with those exceptions.
Predefined classes that work with use() are found in the Java docs for AutoCloseable.
Example:
import atomictest.eq
import checkinstructions.DataFile
fun main() {
DataFile("Results.txt")
.bufferedReader()
.use { it.readLines().first() } eq
"Results"
}
useLines opens a File object, extracts all its lines, and passes those lines to a target function (typically a lambda):
import atomictest.eq
import checkinstructions.DataFile
fun main() {
DataFile("Results.txt").useLines {
it.filter { "*" in it }.first() // [1]
} eq "* ok"
DataFile("Results.txt").useLines { lines ->
lines.filter { line -> // [2]
"*" in line
}.first()
} eq "* ok"
}
[1] The left hand it refers to the collection of lines in the file, while the right hand it refers to each individual line.
[2] Named args prevent confusion from too many its.
useLines() returns the result of the lambda
forEachLine() makes it easy to apply an action to each line in a file:
import checkinstructions.DataFile
import atomictest.*
fun main() {
DataFile("Results.txt").forEachLine {
if (it.startsWith("*"))
trace("$it")
}
trace eq "* ok"
}
The lambda in forEachLine returns Unit
You can create your own class that’s works with use by implementing AutoCloseable:
package resourcecleanup
import atomoictest.*
class Usable(): AutoCloseable {
fun func() = trace("func()")
override fun close() = trace("close()")
}
fun main() {
Usable().use { it.func() }
trace eq "func() close()"
}
AutoCloseable interface only contains the close() function
Logging
You can use an open-source logging package such as Kotlin-logging
First create a logger typically at file scope
package logging
import mu.KLogging
private val log = KLogging().logger
fun main() {
val msg = "Hello, Kotlin Logging!"
log.trace(msg)
log.debug(msg)
log.info(msg)
log.warn(msg)
log..error(msg)
}
Kotin-logging is a facade over Simple Logging Facade for Java (SFL4J) which is an abstraction over multiple logging frameworks.
To report trace() and debug() reporting levels you must change the logging configuration.
Alternatively:
package logging
import checkinstructions.DataFile
val logFile = /// Reset ensures an empty file:
DataFile("simpleLogFile.txt").reset()
fun debug(msg: String) =
System.err.println("Debug: $msg")
//To disable:
// fun debug(msg: String = Unit
fun trace(msg: String) =
logFile.appendText("Trace: $msg\n")
fun main() {
debug("Simple Logging Strategy")
trace("Line 1")
trace("Line 2")
println(logFile.readText())
}
//Sample Output
//Debug: Simple Logging Strategy
//Trace: Line 1
//Trace: Line 2
debug() sends its output to the console error stream, trace sends its output to a log file
Alternatively building a logging class:
package atomiclog
import checkinstructions.DataFile
class Logger(fileName: String) {
val logFile = DataFile(fileName).reset()
private fun log(type: String, msg: String) =
logFile.appendText("$type: $msg\n")
fun trace(msg: String) = log("Trace", msg)
fun debug(msg: String) = log("Debug", msg)
fun info(msg: String) = log("Info", msg)
fun warn(msg: String) = log("Warn", msg)
fun error(msg: String = log("Error", msg)
//For basic testing
fun report(msg: String) {
trace(msg)
debug(msg)
info(msg)
warn(msg)
error(msg)
}
}
You could make this more complex by adding logging levels and timestamps.
Using the library is straightforward:
package useatomiclog
import atomiclog.Logger
import atomictest.eq
private val logger = Logger("AtomicLog.txt")
fun main() {
logger.report("Hello, Atomic Log!")
logger.logFile.readText() eq """
Trace: Hello, Atomic Log!
Debug: Hello, Atomic Log!
Info: Hello, Atomic Log!
Warn: Hello, Atomic Log!
Error: Hello, Atomic Log!
"""
}
Unit Testing
Unit Testing typically tests a small piece of code like a function separately and independently.
To use kotlin.test you must modify the dependencies of your projects 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 and expected values of the function
- assertTrue which tests its first arg, a Boolean expression
- assertNull
- assertNotNull
- assertFails
- assertFailsWith
- expect
- assertNotEquals
- assertFalse
Example:
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")
}
A failing assertion function produces an AssertionError
Example with expect
fun <T> expect(
expected: T,
message: String?
block: () -> T
) {
assertEquals(expected, block(), message)
}
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 sucessfully."
)
assertFailsWith<AssertionError> {
testFortyTwo2(43)
}
capture {
assertFailsWith<AssertionError> {
testFortyTwo2()
}
} contains
listOf("Expected an exception",
"to be thrown",
"but was completed succcessfully."
)
}
To use JUnit5 as the underlying library for kotlin.test add the following dependencies to your build.gradle:
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"
Kotlin supports annotations for definitions and expressions
@Test converts a regular function into a test function
Example:
package unittesting
import kotlin.test.*
class SampleTest {
[@Test](https://micro.blog/Test)
fun testFortyTwo() {
expect(42, "Incorrect,") { fortyTwo() }
}
[@Test](https://micro.blog/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
@Test can be run:
- Indpendently
- As part of a class
- Together with all tests defined for the application
Intellij IDEA allows you to rerun only failed tests
Example defining a state machine:
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)
}
To test StateMachine we create a sm property inside the test class. The test runner creates a fresh StateMachineTest object for each test:
package unittesting
import kotlin.test.*
class StateMachineTest {
val sm = StateMachine()
[@Test](https://micro.blog/Test)
fun start() {
sm.start()
assertEquals(State.On, sm.state)
}
[@Test](https://micro.blog/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)
}
}
backticks allow you to use any character including whitespace in a function name
A CI server can automatically run all available tests and notify you if something breaks
Example of a class with several properties:
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
)
You can add utility functions to make manufacturing test data easier:
package unittesting
import unittesting.Language.*
import kotlin.test.*
fun makeLearner(
id: Int,
language: Language = Kotlin,
name: String = "Test Name $id",
surname: String = "Test Surname $id"
) = Learner(id, name, surname, language)
class LearnerTest {
[@Test](https://micro.blog/Test)
fun `single Learner`() {
val learner = makeLearner(10, Java)
assertEquals("Test Name 10", learner.name)
}
[@Test](https://micro.blog/Test)
fun `multiple Learners'`() {
val learners = (1..9).map(::makeLearner)
assertTrue(
learners.all { it.language == Kotlin }
)
}
}
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 hte same interface as the real one or it can be created using a library such as MockK.
Integration tests ensure different parts of the system workw hen combined.
IntelliJ IDEA and Android STudio support creating and running unit tests.
Right click on a class or function you want to test and click Generate -> Test.
Alternatively open the list of intention actions and select Create Test
Select JUnit5 as the Testing Library, if Junit5 library is not found in the module push Fix.
The destination package should be unittesting
The result will be a another directory, the default is src/test/kotlin, but you can choose a different destination.
Check the boxes next to which function you want etsted
To run tests within IntelliJ IDEA you need to change the default configuration to run tests internally.
Go to File -> Settings -> Build, Execution, Deployment -> Build Tools -> Gradle
Select Run Tests Using IntelliJ IDEA