Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package app.cash.quiver.extensions

import app.cash.quiver.matchers.shouldBeSuccessful
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe

class SuccessfulTest : StringSpec({
"shouldBeSuccessful succeeds on success" {
Result.success().shouldBeSuccessful()
}

"shouldBeSuccessful bubbles up exception on failure" {
val exception = IllegalStateException("oh no!")

shouldThrow<IllegalStateException> {
Result.failure<Throwable>(exception).shouldBeSuccessful()
}.message shouldBe "oh no!"
}

"shouldBeSuccessful with block runs assertions on success value" {
var blockRan = false
val result = Result.success(42).shouldBeSuccessful { value ->
blockRan = true
value shouldBe 42
}
result shouldBe 42
blockRan shouldBe true
}

"shouldBeSuccessful with block bubbles up exception on failure" {
val exception = IllegalStateException("failure!")

shouldThrow<IllegalStateException> {
Result.failure<Int>(exception).shouldBeSuccessful { value ->
value shouldBe 42
}
}.message shouldBe "failure!"
}

"shouldBeSuccessful with expected value succeeds when values match" {
val result = Result.success("hello") shouldBeSuccessful "hello"
result shouldBe "hello"
}

"shouldBeSuccessful with expected value fails when values don't match" {
shouldThrow<AssertionError> {
Result.success("hello") shouldBeSuccessful "world"
}
}

"shouldBeSuccessful with expected value bubbles up exception on failure" {
val exception = IllegalStateException("failed!")

shouldThrow<IllegalStateException> {
Result.failure<String>(exception) shouldBeSuccessful "expected"
}.message shouldBe "failed!"
}
})
3 changes: 3 additions & 0 deletions testing-lib/api/testing-lib.api
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ public final class app/cash/quiver/matchers/MatchersKt {
public static final fun shouldBeInvalid (Larrow/core/Either;)Ljava/util/Set;
public static final fun shouldBePresent (Lapp/cash/quiver/Outcome;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static synthetic fun shouldBePresent$default (Lapp/cash/quiver/Outcome;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun shouldBeSuccessful (Ljava/lang/Object;)Ljava/lang/Object;
public static final fun shouldBeSuccessful (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public static final fun shouldBeSuccessful (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import app.cash.quiver.Present
import app.cash.quiver.extensions.ValidatedNel
import arrow.core.Either
import arrow.core.NonEmptyList
import io.kotest.matchers.shouldBe
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

Expand Down Expand Up @@ -68,3 +69,65 @@ fun <E, A> ValidatedNel<E, A>.shouldBeInvalid(): Set<E> {
is Either.Right -> throw AssertionError("Expected Left (Invalid), but found $this")
}
}

/**
* Assert that the Result is successful and return its unwrapped value.
*
* The primary advantage over Kotest's `shouldBeSuccess` is that this matcher bubbles up
* the original exception when the Result is a failure, rather than wrapping it in an
* AssertionError. This preserves the full stack trace and error context, making test
* failures easier to debug.
*
* Additionally, this matcher returns the unwrapped value, allowing you to use it in
* subsequent assertions or operations without needing to call `getOrNull()` or
* `getOrThrow()` separately.
*
* Example:
* ```
* val userId = getUserResult().shouldBeSuccessful()
* userId shouldBe 42
* ```
*
* @return The unwrapped success value
* @throws Throwable if the Result is a failure, the original exception is thrown (not wrapped)
*/
fun <T> Result<T>.shouldBeSuccessful(): T = this.getOrThrow()

/**
* Assert that the Result is successful and run assertions on its unwrapped value.
*
* This overload is useful when you want to make multiple assertions on the success
* value without needing to unwrap it first. The value is also returned for further use.
*
* Example:
* ```
* Result.success(User(id = 1, name = "Alice")).shouldBeSuccessful { user ->
* user.id shouldBe 1
* user.name shouldBe "Alice"
* }
* ```
*
* @param block A lambda that receives the unwrapped value and can perform assertions on it
* @return The unwrapped success value
* @throws Throwable if the Result is a failure, the original exception is thrown
*/
inline fun <T> Result<T>.shouldBeSuccessful(block: (T) -> Unit): T = this.getOrThrow().also(block)

/**
* Assert that the Result is successful and equals the expected value.
*
* Similar to Kotest's `shouldBeSuccess`, but returns the unwrapped value for further use.
* This enables fluent assertion chains.
*
* Example:
* ```
* val name = getUserName() shouldBeSuccessful "Alice"
* // `name` now contains "Alice" and can be used in subsequent code
* ```
*
* @param expected The expected success value
* @return The unwrapped success value
* @throws AssertionError if the values don't match
* @throws Throwable if the Result is a failure, the original exception is thrown
*/
infix fun <T> Result<T>.shouldBeSuccessful(expected: T): T = this.getOrThrow().also { it.shouldBe(expected) }
Loading