What problem does assertSoftly solve in Kotest, and how does it change the behavior of multiple matcher assertions inside its block?
answer
- Default matchers are fail-fast (hard)
- assertSoftly collects all failures, reports together
- assertSoftly(target){} scopes this to target
- Only matcher failures are soft; real exceptions still abort
- Best for many independent field checks
basics
~20 sNormally a test stops at the first failed check, hiding the rest. assertSoftly runs all the checks in its block, collects every failure, and reports them together at the end, so you see all problems in one run.
solid answer
~50 sBy default Kotest matchers are *hard* assertions: the first failure throws and the test aborts, so you only learn about one problem per run. `assertSoftly { ... }` switches to *soft* assertions — every matcher inside the block executes, failures are accumulated, and a single combined `AssertionError` listing all of them is thrown when the block ends. This is ideal when verifying many fields of one object: `assertSoftly(user) { name shouldBe "Ann"; age shouldBe 30; email shouldEndWith ".com" }`. The receiver overload `assertSoftly(target) { ... }` makes the target `this`, so you call matchers on its members directly. Caveat: only matcher failures are captured; a thrown `NullPointerException` or other exception still aborts the block immediately. Soft assertions don't change matcher semantics, only when failures surface. They reduce edit-run-fix cycles when several assertions are likely to fail together.
code
kotlin · 12 linesimport io.kotest.assertions.assertSoftly
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContain
data class Order(val id: Long, val total: Int, val items: List<String>)
val order = Order(1, 99, listOf("a"))
assertSoftly(order) {
id shouldBe 1
total shouldBe 100 // fails
items shouldContain "b" // also fails -> both reported together
}go deeper
Knows assertSoftly lets multiple checks all run and report together instead of stopping at the first.
Uses the receiver overload and understands hard-vs-soft assertion behavior.
Explains that only matcher failures are accumulated, real exceptions still abort, and when fail-fast is preferable.
Establishes guidance on independent vs dependent assertions to avoid misleading reports and keeps soft assertions from masking real errors.
## The problem: fail-fast hides failures A normal matcher throws an `AssertionError` on the first failure. If a test has five assertions and the first fails, you fix it, re-run, then discover the second fails, and so on — slow and frustrating. This is **hard** (fail-fast) assertion behavior. ## What `assertSoftly` does `assertSoftly { block }` collects **all** matcher failures inside the block and throws **one** combined `AssertionError` at the end listing every failure. This is a **soft** assertion scope. ```kotlin import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldEndWith assertSoftly { response.status shouldBe 200 response.body shouldContain "ok" response.header("X-Id") shouldEndWith "-v2" } // if all three are wrong, the report shows all three ``` ## Receiver overload There is an overload `assertSoftly(target) { /* this == target */ }` that scopes the block to an object so you can assert on its members concisely: ```kotlin assertSoftly(user) { name shouldBe "Ann" age shouldBe 30 roles shouldContain "admin" } ``` ## Important caveat: only matcher failures are soft Soft assertions accumulate **matcher** failures. If a line throws a *real* exception — e.g. a `NullPointerException` from `user.profile!!.bio`, an `IndexOutOfBoundsException`, or an exception from the code under test — that propagates immediately and aborts the block; later assertions don't run. So `assertSoftly` is not a try/catch around arbitrary code. ## Nesting and composition Soft-assertion scopes can be nested; failures bubble up to the outermost scope and are reported together. `assertSoftly` works in any spec style and pairs naturally with data-class field checks. ## When NOT to use it If a later assertion is meaningless once an earlier one fails (e.g. you assert a list is non-empty, then index into it), keep them hard so you don't hit a confusing exception — or guard the dependent assertion. Soft assertions are best for **independent** checks on the same subject.
- Does assertSoftly catch a NullPointerException thrown inside its block?No. It only accumulates matcher failures; a real thrown exception propagates immediately and aborts the block.
- When is fail-fast (no assertSoftly) actually better?When later assertions depend on an earlier one holding — e.g. asserting non-empty then indexing — so a failure wouldn't cause a confusing downstream exception.
- What's the benefit of the assertSoftly(target){} overload?It makes target the receiver (this), so you assert on its members without repeating the variable name.
Like a proofreader marking every typo on the page at once, instead of stopping and handing the page back at the first one.
saying these in an interview costs you the question
- Claiming assertSoftly turns any exception into a soft failure
- Using it around assertions where later ones depend on earlier ones
- Thinking it changes equality/matcher semantics rather than just failure timing
- Expecting partial results when the code under test itself throws inside the block