Skip to content

Commit

Permalink
New mechanism for testing thrown exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
robfletcher committed Sep 16, 2018
1 parent db1997d commit 1b67725
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 115 deletions.
1 change: 1 addition & 0 deletions site/src/jbake/content/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cached=true
Strikt was written by [Rob Fletcher](https://github.com/robfletcher) with contributions from:
* [Xavier Hanin](https://github.com/xhanin)
* [Paul Merlin](https://github.com/eskatos)
* [Christoph Sturm](https://github.com/christophsturm)
Expand Down
18 changes: 11 additions & 7 deletions site/src/jbake/content/user-guide/expecting-exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,24 @@ nextPage=flow-typing.html
# Asserting exceptions are thrown
To assert that some code throws an exception you can use an assertion on a lambda `() -> Unit` that performs the operation that should throw an exception and the `throws<E>` assertion function.
To assert that some code throws an exception you can use the `catching` function that accepts a lambda `() -> Unit` that performs the operation that should throw an exception and the `throws<E>` assertion function.
For example:
```kotlin
expectThat { service.computeMeaning() }
.throws<TooMuchFlaxException>()
expectThat(catching { service.identifyHotdog() })
.throws<NotHotdogException>()
```
The `catching` function simply returns `Throwable?` with the value being whatever exception is thrown, or `null` if nothing is thrown.
Combining it with the `throws<E>` assertion allows testing for specific exception types.
The `throws<E>` assertion will fail if the exception is `null` or the wrong type.
The `throws<E>` function returns an `Assertion.Builder<E>` so you can chain assertions about the exception after it.
There is also a top level function `expectThrows(() -> Unit)` that makes this even more concise.
If you just need to test that _any_ exception was or was not thrown you can combine `catching` with `isNull` or `isNotNull`.
For example:
```kotlin
expectThrows<TooMuchFlaxException> {
service.computeMeaning()
}
expectThat(catching { service.identifyHotdog() })
.isNull()
```
18 changes: 18 additions & 0 deletions strikt-core/src/main/kotlin/strikt/api/Catching.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package strikt.api

/**
* Executes [action], catching and returning any exception that is thrown. If
* no exception is thrown the method returns `null`.
*
* @return the exception thrown by [action] or `null` if no exception is thrown.
*/
fun catching(
action: () -> Unit
): Throwable? {
return try {
action()
null
} catch (actual: Throwable) {
actual
}
}
12 changes: 5 additions & 7 deletions strikt-core/src/main/kotlin/strikt/api/Expect.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,11 @@ fun <T> expectThat(
* @return an assertion over the thrown exception, allowing further assertions
* about messages, root causes, etc.
*/
@Deprecated(
"Use the catching function with expectThat",
replaceWith = ReplaceWith("expectThat(catching(action)).throws<E>()")
)
inline fun <reified E : Throwable> expectThrows(
noinline action: () -> Unit
): Builder<E> =
expectThat(action).throws()

/**
* special case expectThat method to fix blocks that don't return Unit
*/
fun expectThat(subject: () -> Unit): DescribeableBuilder<() -> Unit> =
AssertionBuilder(AssertionSubject(subject), AssertionStrategy.Throwing)
expectThat(catching(action)).throws()
39 changes: 0 additions & 39 deletions strikt-core/src/main/kotlin/strikt/assertions/Functions.kt

This file was deleted.

26 changes: 24 additions & 2 deletions strikt-core/src/main/kotlin/strikt/assertions/Throwable.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
package strikt.assertions

import strikt.api.Assertion.Builder
import strikt.api.DescribeableBuilder

/**
* Maps an assertion on a [Throwable] to an assertion on its message.
* Maps an assertion on a [Throwable] to an assertion on its
* [Throwable.message].
* This mapping also asserts that the message is not `null`.
* This is particularly useful after [throws].
*
* @author [Xavier Hanin](https://github.com/xhanin)
*/
val <T : Throwable> Builder<T>.message: Builder<String>
get() = map(Throwable::message).isNotNull()

/**
* Maps an assertion on a [Throwable] to an assertion on its [Throwable.cause].
*/
val <T : Throwable> Builder<T>.cause: DescribeableBuilder<Throwable?>
get() = map(Throwable::cause)

/**
* Asserts that an exception is an instance of the expected type.
* The assertion fails if the subject is `null` or not an instance of [E].
*
* This assertion is designed for use with the [strikt.api.catching] function.
*/
inline fun <reified E : Throwable> Builder<Throwable?>.throws(): Builder<E> =
assert("threw %s", E::class.java) {
when (it) {
null -> fail("nothing was thrown")
is E -> pass()
else -> fail(description = "%s was thrown", actual = it, cause = it)
}
} as Builder<E>
8 changes: 6 additions & 2 deletions strikt-core/src/test/kotlin/strikt/Exceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,9 @@ class Exceptions {
fun `expected and actual are undefined if a failure does not specify an actual`() {
fails {
expectThat("fnord")
.assert("is %s", "something") { fail("o noes") }
.assert("is %s", "something") {
fail("o noes")
}
}.let { error ->
expectThat(error)
.isA<AssertionFailedError>()
Expand All @@ -246,7 +248,9 @@ class Exceptions {
fun `expected and actual are defined if a failure specifies an actual`() {
fails {
expectThat("fnord")
.assert("is %s", "something") { fail("something else", "o noes") }
.assert("is %s", "something") {
fail("something else", "o noes")
}
}.let { error ->
expectThat(error)
.isA<AssertionFailedError>()
Expand Down
33 changes: 18 additions & 15 deletions strikt-core/src/test/kotlin/strikt/Mapping.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import strikt.api.catching
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.containsExactly
import strikt.assertions.first
import strikt.assertions.get
Expand All @@ -14,6 +14,7 @@ import strikt.assertions.isNotNull
import strikt.assertions.isNull
import strikt.assertions.last
import strikt.assertions.message
import strikt.assertions.throws
import java.time.LocalDate

@DisplayName("mapping assertions")
Expand Down Expand Up @@ -51,24 +52,26 @@ internal class Mapping {

@Test
fun `message maps to an exception message`() {
expectThrows<IllegalStateException> {
throw IllegalStateException("o noes")
}.message.isEqualTo("o noes")
expectThat(catching { throw IllegalStateException("o noes") })
.throws<IllegalStateException>()
.message
.isEqualTo("o noes")
}

@Test
fun `message fails if the exception message is null`() {
expectThrows<AssertionError> {
expectThrows<IllegalStateException> {
throw IllegalStateException()
}.message
}.message.isEqualTo(
"▼ Expect that () -> kotlin.Unit:\n" +
" ✓ throws java.lang.IllegalStateException\n" +
" ▼ thrown exception:\n" +
" ▼ value of property message:\n" +
" ✗ is not null"
)
fails {
expectThat(catching { throw IllegalStateException() })
.throws<IllegalStateException>()
.message
}.let { error ->
expectThat(error).message.isEqualTo(
"▼ Expect that java.lang.IllegalStateException:\n" +
" ✓ threw java.lang.IllegalStateException\n" +
" ▼ value of property message:\n" +
" ✗ is not null"
)
}
}

data class Person(val name: String, val birthDate: LocalDate)
Expand Down
59 changes: 16 additions & 43 deletions strikt-core/src/test/kotlin/strikt/Throws.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,81 +3,54 @@ package strikt
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.opentest4j.AssertionFailedError
import strikt.api.catching
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isA
import strikt.assertions.throws

@DisplayName("throws assertion")
internal class Throws {
@Test
fun `throws passes if the action throws the expected exception`() {
expectThrows<IllegalStateException> { -> throw IllegalStateException() }
expectThat(catching { throw IllegalStateException() })
.throws<IllegalStateException>()
}

@Test
fun `throws passes if the action throws a sub-class of the expected exception`() {
expectThrows<RuntimeException> { -> throw IllegalStateException() }
expectThat(catching { throw IllegalStateException() })
.throws<RuntimeException>()
}

@Test
fun `throws fails if the action does not throw any exception`() {
fails {
expectThrows<IllegalStateException> { -> }
expectThat(catching { })
.throws<IllegalStateException>()
}.let { e ->
val expected = "▼ Expect that () -> kotlin.Unit:\n" +
"throws java.lang.IllegalStateException : nothing was thrown"
val expected = "▼ Expect that null:\n" +
"threw java.lang.IllegalStateException : nothing was thrown"
assertEquals(expected, e.message)
}
}

@Test
fun `throws fails if the action throws the wrong type of exception`() {
assertThrows<AssertionFailedError> {
expectThrows<IllegalStateException> { -> throw NullPointerException() }
fails {
expectThat(catching { throw NullPointerException() })
.throws<IllegalStateException>()
}.let { e ->
val expected = "▼ Expect that () -> kotlin.Unit:\n" +
"throws java.lang.IllegalStateException : java.lang.NullPointerException was thrown"
val expected = "▼ Expect that java.lang.NullPointerException:\n" +
"threw java.lang.IllegalStateException : java.lang.NullPointerException was thrown"
assertEquals(expected, e.message)
assertEquals(NullPointerException::class.java, e.cause?.javaClass)
}
}

@Test
fun `throws returns an assertion whose subject is the exception that was caught`() {
expectThrows<IllegalStateException> { -> throw IllegalStateException() }
expectThat(catching { -> throw IllegalStateException() })
.throws<IllegalStateException>()
.isA<IllegalStateException>()
}

@Test
fun `throws formats the message for a callable reference`() {
class Thing {
fun throwSomething() {
throw NullPointerException()
}

override fun toString(): String = "MyThing"
}
fails {
val subject = Thing()
val fn: () -> Unit = subject::throwSomething
expectThat(fn).throws<IllegalStateException>()
}.let { e ->
val expected = "▼ Expect that MyThing::throwSomething:\n" +
" ✗ throws java.lang.IllegalStateException : java.lang.NullPointerException was thrown"
assertEquals(expected, e.message)
}
}

@Test
fun `expect - throws works with blocks that don't return unit`() {
fails {
expectThat {
@Suppress("UNUSED_EXPRESSION")
"String"
}.throws<IllegalStateException>()
}
}
}

0 comments on commit 1b67725

Please sign in to comment.