You're designing a tiny assertion DSL with `infix fun <T> T.shouldBe(expected: T)`. Walk through how the infix call resolves, generics, nullability, and one limitation you'd warn the team about.
answer
- a shouldBe b == a.shouldBe(b)
- single T infers common supertype → unsafe `1 shouldBe "x"` compiles
- use == (equals), beware NaN/floats/arrays
- infix left-associative, no clean chaining
- Kotest is the reference
basics
~20 sactual shouldBe expected calls actual.shouldBe(expected). The generic T is inferred from both sides, so types must line up. You should warn that infix doesn't chain cleanly and equality uses equals, which can surprise with platform types or floating-point.
solid answer
~40 s`infix fun <T> T.shouldBe(expected: T)` is an extension on `T`; `actual shouldBe expected` desugars to `actual.shouldBe(expected)`. `T` is inferred from the receiver and argument via a common supertype — `1 shouldBe 1` infers `Int`, but `1 shouldBe "x"` infers `Comparable<*> & Serializable` (or `Any`), so it compiles yet always fails — a real type-safety hole worth a stricter signature or contracts. Inside, comparison should use structural equality (`==`, i.e. `equals`), being careful with `Double.NaN`, floating-point tolerance, and arrays (use `contentEquals`). Nullability: `T` can be nullable so `x shouldBe null` works. Limitations to flag: infix is **left-associative and unparenthesized**, so you can't naturally chain (`a shouldBe b shouldBe c` is `(a shouldBe b) shouldBe c`), and it competes with arithmetic precedence. Kotest's matcher DSL is the production reference for this pattern.
code
kotlin · 8 linesinfix fun <T> T.shouldBe(expected: T) {
if (this != expected)
throw AssertionError("expected <$expected> but was <$this>")
}
42 shouldBe 42 // ok
listOf(1) shouldBe listOf(1) // structural equality, ok
// 1 shouldBe "x" // compiles (T = Comparable<*> & ...), always fails -> type holego deeper
Knows the infix call equals a method call and compares with ==.
Explains generic inference basics and nullability of T.
Surfaces the single-T type-safety hole, equality edge cases, and infix chaining/precedence limits.
Designs the matcher API trade-offs (type safety vs ergonomics, message quality via inline/contracts) and cites Kotest-style precedent for the team.
## Resolution ```kotlin infix fun <T> T.shouldBe(expected: T) { if (this != expected) throw AssertionError("expected <$expected> but was <$this>") } result shouldBe 42 // result.shouldBe(42) ``` The infix call `a shouldBe b` is exactly `a.shouldBe(b)`. Because it is an **extension function**, `a` is the receiver bound to `this` inside the body. ## Generics and inference `T` is inferred from **both** the receiver and the argument by finding their least common supertype: - `1 shouldBe 1` → `T = Int`. - `1 shouldBe 1L` → no common numeric supertype besides `Comparable<*>`/`Number & ...`; often a warning or it infers a broad type. - `"a" shouldBe 1` → `T` infers to a common supertype like `Comparable<*> & Serializable`, so it **compiles** but the assertion always fails. This is the classic type-safety gap of a single-`T` signature. To tighten it you can split the receiver and expected types or rely on more precise overloads, accepting some loss of ergonomics. ## Nullability `T` may be inferred as a nullable type, so `value shouldBe null` is legal and uses null-safe `==`. If you want to forbid null expectations, constrain `T : Any`. ## Equality correctness Use structural equality `==` (which calls `equals`), not reference `===`. Edge cases to handle: - **Floating point:** `0.1 + 0.2 shouldBe 0.3` fails; provide a tolerance matcher. - **`Double.NaN`:** `NaN == NaN` is false, but `NaN.equals(NaN)` (boxed) is true — be deliberate. - **Arrays:** `==` compares identity; use `contentEquals`/`contentDeepEquals`. ## Limitations to warn the team about - **No clean chaining:** infix is left-associative, so `a shouldBe b shouldBe c` parses as `(a shouldBe b) shouldBe c` — usually a mistake. - **Precedence:** infix binds looser than arithmetic, tighter than comparisons; mixing without parentheses can surprise. - **Discoverability:** infix members don't show as obviously in autocomplete after a space the way `.method` does. - **Error messages:** without inline/contracts, failure messages and stack traces point at the helper, not the test line; consider `kotlin.contracts` or inline + good messages. ## Production reference Kotest's `a shouldBe b`, `a shouldNotBe b`, and matcher infix functions are the canonical real-world implementation of exactly this pattern.
- Why does `1 shouldBe "hello"` compile?Inference picks a common supertype of Int and String (e.g. Comparable<*> & Serializable), so a single-`T` signature accepts both sides; the assertion then always fails at runtime.
- How would you make failure messages point at the test line instead of the helper?Mark it `inline` so the body is inlined into the test, and/or attach source-location info; some frameworks capture the call site explicitly.
saying these in an interview costs you the question
- Using `===` (reference equality) instead of `==` for value comparison
- Claiming `1 shouldBe "x"` is a compile error (it isn't with a single T)
- Forgetting array/`NaN`/float equality pitfalls
- Assuming infix chains like a fluent builder
- Not mentioning Kotest or any real precedent