Skip to content

Commit f000926

Browse files
committed
Merge branch 'docs-update'
2 parents cf5a185 + d9d2bab commit f000926

1 file changed

Lines changed: 147 additions & 47 deletions

File tree

website/docs/Guides/Answers.mdx

Lines changed: 147 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -105,66 +105,66 @@ everySuspend { repository.findById(id = any()) } throwsErrorWith "Failed!"
105105
You can define a sequence of answers using `sequentially`:
106106

107107
```kotlin
108-
everySuspend { repository.getById(id = any()) } sequentially {
108+
everySuspend { repository.findById(id = any()) } sequentially {
109109
returns(stubBook("1"))
110110
calls { stubBook("2") }
111111
throws(IllegalStateException())
112112
}
113-
repository.getById("1") // returns stubBook("1")
114-
repository.getById("2") // returns stubBook("2")
115-
runCatching { repository.getById("3") } // throws IllegalStateException
116-
repository.getById("4") // fails - no more answers
113+
repository.findById("1") // returns stubBook("1")
114+
repository.findById("2") // returns stubBook("2")
115+
runCatching { repository.findById("3") } // throws IllegalStateException
116+
repository.findById("4") // fails - no more answers
117117
```
118118

119119
At the end of `sequentially` block you can repeat a sequence of answers with `repeat`:
120120

121121
```kotlin
122-
everySuspend { repository.getById(id = any()) } sequentially {
122+
everySuspend { repository.findById(id = any()) } sequentially {
123123
returns(stubBook("1"))
124124
repeat { returns(stubBook("2")) }
125125
}
126-
repository.getById("1") // returns stubBook("1")
127-
repository.getById("2") // returns stubBook("2")
128-
repository.getById("3") // returns stubBook("2")
129-
repository.getById("4") // returns stubBook("2")
126+
repository.findById("1") // returns stubBook("1")
127+
repository.findById("2") // returns stubBook("2")
128+
repository.findById("3") // returns stubBook("2")
129+
repository.findById("4") // returns stubBook("2")
130130
```
131131

132132
You can use `sequentiallyRepeat` as a shorthand if you want to define repeating sequence:
133133
```kotlin
134-
everySuspend { repository.getById(id = any()) } sequentiallyRepeat {
134+
everySuspend { repository.findById(id = any()) } sequentiallyRepeat {
135135
returns(stubBook("1"))
136136
returns(stubBook("2"))
137137
}
138-
repository.getById("1") // returns stubBook("1")
139-
repository.getById("2") // returns stubBook("2")
140-
repository.getById("3") // returns stubBook("1")
141-
repository.getById("4") // returns stubBook("2")
138+
repository.findById("1") // returns stubBook("1")
139+
repository.findById("2") // returns stubBook("2")
140+
repository.findById("3") // returns stubBook("1")
141+
repository.findById("4") // returns stubBook("2")
142142
```
143143

144144
You can use `sequentiallyReturns` and `sequentiallyThrows` as a shorthand:
145145
```kotlin
146-
everySuspend { repository.getById(id = any()) } sequentiallyReturns listOf(stubBook("1"), stubBook("2"))
147-
repository.getById("1") // returns stubBook("1")
148-
repository.getById("2") // returns stubBook("2")
149-
repository.getById("3") // fails - no more answers
146+
everySuspend { repository.findById(id = any()) } sequentiallyReturns listOf(stubBook("1"), stubBook("2"))
147+
repository.findById("1") // returns stubBook("1")
148+
repository.findById("2") // returns stubBook("2")
149+
repository.findById("3") // fails - no more answers
150150
```
151151

152152
You can nest `sequentially` calls:
153153

154154
```kotlin
155-
everySuspend { repository.getById(id = any()) } sequentially {
155+
everySuspend { repository.findById(id = any()) } sequentially {
156156
returns(stubBook("1"))
157157
sequentially {
158158
returns(stubBook("2"))
159159
returns(stubBook("3"))
160160
}
161161
returns(stubBook("4"))
162162
}
163-
repository.getById("1") // returns stubBook("1")
164-
repository.getById("2") // returns stubBook("2")
165-
repository.getById("3") // returns stubBook("3")
166-
repository.getById("4") // returns stubBook("4")
167-
repository.getById("5") // fails - no more answers
163+
repository.findById("1") // returns stubBook("1")
164+
repository.findById("2") // returns stubBook("2")
165+
repository.findById("3") // returns stubBook("3")
166+
repository.findById("4") // returns stubBook("4")
167+
repository.findById("5") // fails - no more answers
168168
```
169169
### Calling original implementation
170170

@@ -243,53 +243,153 @@ everySuspend { repository.findById(any()) } calls {
243243
}
244244
```
245245

246-
### Custom answer
246+
### Extending answers API
247247

248-
To provide custom answer implement [Answer](pathname:///api_reference/mokkery-runtime/dev.mokkery.answering/-answer/index.html):
248+
The answers API provides a way to define custom behavior for mocked method calls in Mokkery.
249+
It is built around two core interfaces:
250+
251+
- [AnsweringScope](https://mokkery.dev/api_reference/mokkery-runtime/dev.mokkery.answering/-answering-scope/index.html)
252+
- [Answer](https://mokkery.dev/api_reference/mokkery-runtime/dev.mokkery.answering/-answer/index.html)
249253

250254
```kotlin
251-
object RandomIntAnswer : Answer<Int> {
255+
@DelicateMokkeryApi
256+
public interface Answer<out T> {
257+
258+
public fun call(scope: MokkeryBlockingCallScope): T
252259

253-
override fun call(scope: FunctionScope) = Random.nextInt()
260+
public suspend fun call(scope: MokkerySuspendCallScope): T
261+
}
262+
263+
// ...
264+
265+
public interface AnsweringScope<T> {
266+
267+
@DelicateMokkeryApi
268+
public infix fun answers(answer: Answer<T>)
254269
}
255270
```
256271

257-
Answer that implements only `call` works for both regular functions and suspending functions.
272+
#### Core concepts
273+
274+
The `Answer` interface represents custom behavior for mocked calls.
275+
It has two `call` overloads:
276+
- `call(MokkeryBlockingCallScope)` – used for regular (blocking) functions.
277+
- `call(MokkerySuspendCallScope)` – used for suspend functions.
278+
279+
The `AnsweringScope` interface provides the `answers` method, which registers an `Answer` for a particular call.
280+
281+
Some custom answers may work with both regular and suspend methods, while others support only one type.
282+
However, `AnsweringScope` itself does not enforce any restrictions - you can technically register any `Answer` for any call.
283+
284+
This flexibility can lead to misuse, such as registering a suspend-only answer for a blocking call.
285+
For this reason, both `AnsweringScope.answers` and the `Answer` interface are marked as *delicate* APIs.
286+
287+
#### Restricting usage
288+
289+
To provide compile-time safety, Mokkery defines two subtypes of `AnsweringScope`:
258290

259291
```kotlin
260-
everySuspend { repository.countAllBooks() } answers RandomIntAnswer
292+
public interface SuspendAnsweringScope<T> : AnsweringScope<T>
293+
294+
public interface BlockingAnsweringScope<T> : AnsweringScope<T>
261295
```
262296

263-
You can provide convenient extension for your custom answer:
297+
These are marker interfaces - they don’t add extra methods but indicate whether the context is for
298+
a regular method (`BlockingAnsweringScope`), or a suspend method (`SuspendAnsweringScope`).
299+
300+
Mokkery uses them as follows:
301+
- `every` returns a `BlockingAnsweringScope`
302+
- `everySuspend` returns a `SuspendAnsweringScope`
303+
304+
Custom `Answer` implementations should not be used directly.
305+
Instead, expose them through convenience extensions on the appropriate scope type.
306+
307+
Example: a suspend-only answer that delays before returning a value:
264308

265309
```kotlin
266-
fun AnsweringScope<Int>.returnsRandomInt() = answers(RandomIntAnswer)
267-
268-
// ...
310+
infix fun SuspendAnsweringScope<T>.returnsDelayed(value: T) {
311+
answers(ReturnsDelayedAnswer(value))
312+
}
313+
314+
// you can optionally mark this class as private
315+
private class ReturnsDelayedAnswer<T>(private val value: T) : Answer<T> {
316+
public fun call(scope: MokkeryBlockingCallScope): T = error("Not supported")
317+
318+
public suspend fun call(scope: MokkerySuspendCallScope): T {
319+
delay(1_000)
320+
return value
321+
}
322+
}
323+
324+
// Usage:
325+
everySuspend { repository.findById(any()) } returnsDelayed stubBook()
326+
// ✅ compiles - suspend context
327+
328+
every { repository.findAll() } returnsDelayed flowOf(stubBook())
329+
// ❌ does NOT compile - regular context
330+
```
331+
332+
Some answers are valid for both regular and suspend methods.
333+
For consistency, you should still declare an extension function, but this time use `AnsweringScope` as the receiver:
334+
335+
```kotlin
336+
infix fun AnsweringScope<T>.returns(value: T) {
337+
answers(ReturnsAnswer(value))
338+
}
339+
340+
private class ReturnsAnswer<T>(private val value: T) : Answer<T> {
341+
public fun call(scope: MokkeryBlockingCallScope): T = value
342+
public suspend fun call(scope: MokkerySuspendCallScope): T = value
343+
}
269344

270-
everySuspend { repository.countAllBooks() }.returnsRandomInt()
345+
// Usage:
346+
everySuspend { repository.findById(any()) } returns stubBook() // suspend
347+
every { repository.findAll() } returns flowOf(stubBook()) // regular
348+
// ✅ both compile
271349
```
272350

273-
If your answer needs suspension, it is recommended to implement [Answer.Suspending](pathname:///api_reference/mokkery-runtime/dev.mokkery.answering/-answer/-suspending/index.html):
351+
#### Using existing answers
352+
353+
If your custom answer can be expressed using other built-in answers, you don’t need to create a new `Answer` implementation.
354+
Instead, declare an extension using the existing calls function:
274355

275356
```kotlin
276-
data class DelayedConstAnswer<T>(
277-
val value: T,
278-
) : Answer.Suspending<T> {
279-
280-
override suspend fun callSuspend(scope: FunctionScope): T {
357+
fun <T> SuspendAnsweringScope<T>.returnsDelayed(value: T) {
358+
calls {
281359
delay(1_000)
282-
return value
360+
value
283361
}
284362
}
285363
```
286364

287-
For suspending answers it's highly recommended to introduce convenience extension. It's able to indicate that given answer only supports suspending functions:
365+
This is simpler and avoids extra boilerplate.
288366

367+
#### Answer description
368+
369+
The `Answer` interface lets you implement a `description()` method, which provides a human-readable explanation of the answer.
370+
This is especially useful for debugging, e.g., with [printMokkeryDebug](https://mokkery.dev/api_reference/mokkery-runtime/dev.mokkery.debug/print-mokkery-debug.html)
371+
372+
The description should resemble the code usage:
289373
```kotlin
290-
infix fun <T> SuspendAnsweringScope<T>.returnsAfterDelay(value: T) = answers(DelayedConstAnswer(value))
374+
infix fun SuspendAnsweringScope<T>.returnsDelayed(value: T) {
375+
answers(ReturnsDelayedAnswer(value))
376+
}
377+
291378
// ...
292-
everySuspend { repository.countAllBooks() } returnsAfterDelay 1
379+
private class ReturnsDelayedAnswer<T>(private val value: T) : Answer<T> {
380+
// ...
381+
override fun description() = "returnsDelayed $value"
382+
}
293383
```
294384

295-
If you want to restrict answer only for regular functions, use `BlockingAnsweringScope<T>`.
385+
#### Answer implementation helpers
386+
387+
When implementing custom answers, these helper interfaces can useful:
388+
389+
- [Answer.Suspending](https://mokkery.dev/api_reference/mokkery-runtime/dev.mokkery.answering/-answer/-suspending/index.html) - For suspend-only answers.
390+
Provides a default blocking implementation that throws an exception.
391+
- [Answer.Blocking](https://mokkery.dev/api_reference/mokkery-runtime/dev.mokkery.answering/-answer/-blocking/index.html) - For blocking-only answers.
392+
Provides a default suspend implementation that throws an exception.
393+
- [Answer.Unified](https://mokkery.dev/api_reference/mokkery-runtime/dev.mokkery.answering/-answer/-unified/index.html) - Adds a third call overload with MokkeryCallScope.
394+
The other two overloads delegate to this method, so you only need to implement one function.
395+
Useful when the same logic applies to both blocking and suspend contexts.

0 commit comments

Comments
 (0)