Exceptions and throwables are essential part of many Java APIs. They are useful for modeling application domain layer and controlling program execution flow.
We’ll be working on following piece of code:
class CustomException : Exception()class SystemUnderTest { fun doStuff() { throw CustomException() }}
Annotated Junit4 method
class AssertionsOnExceptionsTest { @Test(expected = CustomException::class) fun `it should throw exception`() { val systemUnderTest = SystemUnderTest() systemUnderTest.doStuff() }}
Not perfect – the whole test method listens for `CustomException` to be thrown anywhere in method – and with junit4 tools we don’t have simple way to perform more specific assertion on that exception.
Junit 5 – assertThrows{}
"it should throw exception - junit5"{ val exception = Assertions.assertThrows(CustomException::class.java) { systemUnderTest.doStuff() } Assertions.assertEquals("Something broke!", exception.message)}
Junit 5 API is more straightforward – it listens for exception to be thrown inside Executable block in assertThrows method. Under the hood it intercepts throwable via try-catch block and throws assertion error if there was no matching exception caught.
Try-catch and runCatching
class AssertionsOnExceptionsSpec : StringSpec({ val systemUnderTest = SystemUnderTest() "it should throw exception - try-catch"{ var exception: CustomException? = null try { systemUnderTest.doStuff() } catch (e: CustomException) { exception = e } assertNotNull(exception) } "it should throw exception - runCatching"{ val exception = kotlin.runCatching { systemUnderTest.doStuff() }.exceptionOrNull() assertNotNull(exception) }})
Try-catch block – standard way to catch exceptions in Java. In this snippet CustomException is caught in catch block, then assigned to mutable variable defined before this block. Not very elegant solution, but it get’s the work done.
We can additionally use runCatching function from Kotlin standard library – this function returns Result<T> of given type (in this particular example this would be Unit type) or return Throwable emitted in that block. Then, we can get that exception from result and perform some extra assertions on it.
Kotest – shouldThrow
class AssertionsOnExceptionsSpec : StringSpec({ val systemUnderTest = SystemUnderTest() "it should throw exception - Kotest: shouldThrow"{ shouldThrow<CustomException> { systemUnderTest.doStuff() } }})
In Kotest (KotlinTest) there is already built-in assertion on exception – shouldThrow<>. It does a few things
- under the hood it uses try-catch block to resolve thrown exception
- it throws AssertionError if no exception was thrown in that block
- it handles case where AssertionError would be thrown inside that block
- it returns exception that was thrown so we are able to perform additional checks
"it should throw exception - kotest: shouldThrow"{ val exception = shouldThrow<CustomException> { systemUnderTest.doStuff() } exception.message shouldBe "I'm broken"}
Few things to remember
- remember that unhandled exception stops test execution
- don’t catch too generic exception – you may swallow AssertionException and get not proper test result