@@ -105,66 +105,66 @@ everySuspend { repository.findById(id = any()) } throwsErrorWith "Failed!"
105105You 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
119119At 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
132132You 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
144144You 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
152152You 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