0
# Nondeterministic Testing
1
2
Support for testing asynchronous and eventually consistent systems with configurable retry strategies and timing controls.
3
4
## Capabilities
5
6
### Eventually Functions
7
8
Test assertions that may initially fail but should eventually succeed within a time limit.
9
10
```kotlin { .api }
11
/**
12
* Repeatedly execute test until it passes or timeout is reached
13
* Uses default configuration (5 seconds duration, 25ms interval)
14
* @param test The test code to execute repeatedly
15
* @return The result of the successful test execution
16
*/
17
suspend fun <T> eventually(test: suspend () -> T): T
18
19
/**
20
* Repeatedly execute test until it passes or specified duration elapses
21
* @param duration Maximum time to keep retrying
22
* @param test The test code to execute repeatedly
23
* @return The result of the successful test execution
24
*/
25
suspend fun <T> eventually(duration: Duration, test: suspend () -> T): T
26
27
/**
28
* Repeatedly execute test with custom configuration
29
* @param config Configuration for retry behavior
30
* @param test The test code to execute repeatedly
31
* @return The result of the successful test execution
32
*/
33
suspend fun <T> eventually(config: EventuallyConfiguration, test: suspend () -> T): T
34
```
35
36
**Usage Examples:**
37
38
```kotlin
39
import io.kotest.assertions.nondeterministic.eventually
40
import io.kotest.matchers.shouldBe
41
import kotlin.time.Duration.Companion.seconds
42
43
// Basic eventually with defaults (5 second timeout)
44
eventually {
45
remoteService.isHealthy() shouldBe true
46
}
47
48
// Custom timeout
49
eventually(10.seconds) {
50
database.countRecords() shouldBe 100
51
}
52
53
// With custom configuration
54
val config = eventuallyConfig {
55
duration = 30.seconds
56
interval = 100.milliseconds
57
initialDelay = 1.seconds
58
}
59
60
eventually(config) {
61
queue.isEmpty() shouldBe true
62
}
63
```
64
65
### Continually Functions
66
67
Test assertions that should remain true for a specified duration.
68
69
```kotlin { .api }
70
/**
71
* Repeatedly execute test and verify it continues to pass
72
* @param test The test code to execute repeatedly
73
* @return The result of the test execution
74
*/
75
suspend fun <T> continually(test: suspend () -> T): T
76
77
/**
78
* Repeatedly execute test for specified duration
79
* @param duration How long to keep testing
80
* @param test The test code to execute repeatedly
81
* @return The result of the test execution
82
*/
83
suspend fun <T> continually(duration: Duration, test: suspend () -> T): T
84
85
/**
86
* Repeatedly execute test with custom configuration
87
* @param config Configuration for continual testing
88
* @param test The test code to execute repeatedly
89
* @return The result of the test execution
90
*/
91
suspend fun <T> continually(config: ContinuallyConfiguration, test: suspend () -> T): T
92
```
93
94
**Usage Examples:**
95
96
```kotlin
97
import io.kotest.assertions.nondeterministic.continually
98
import io.kotest.matchers.shouldBe
99
100
// Verify service stays healthy for 30 seconds
101
continually(30.seconds) {
102
healthCheck.status shouldBe "OK"
103
}
104
105
// Verify memory usage stays under limit
106
continually {
107
Runtime.getRuntime().freeMemory() should beGreaterThan(1000000)
108
}
109
```
110
111
### Until Functions
112
113
Execute test repeatedly until it returns without throwing an exception.
114
115
```kotlin { .api }
116
/**
117
* Execute test repeatedly until it succeeds (doesn't throw)
118
* @param test The test code to execute repeatedly
119
* @return The result of the successful test execution
120
*/
121
suspend fun <T> until(test: suspend () -> T): T
122
123
/**
124
* Execute test repeatedly until it succeeds or timeout
125
* @param duration Maximum time to keep trying
126
* @param test The test code to execute repeatedly
127
* @return The result of the successful test execution
128
*/
129
suspend fun <T> until(duration: Duration, test: suspend () -> T): T
130
131
/**
132
* Execute test repeatedly with custom configuration
133
* @param config Configuration for retry behavior
134
* @param test The test code to execute repeatedly
135
* @return The result of the successful test execution
136
*/
137
suspend fun <T> until(config: UntilConfiguration, test: suspend () -> T): T
138
```
139
140
### Configuration Classes
141
142
Configuration objects for customizing retry behavior and intervals.
143
144
```kotlin { .api }
145
/**
146
* Configuration for eventually testing
147
*/
148
data class EventuallyConfiguration(
149
val duration: Duration = 5.seconds,
150
val interval: Duration = 25.milliseconds,
151
val initialDelay: Duration = Duration.ZERO,
152
val listener: EventuallyListener = NoopEventuallyListener
153
)
154
155
/**
156
* Builder for creating EventuallyConfiguration
157
*/
158
class EventuallyConfigurationBuilder {
159
var duration: Duration = 5.seconds
160
var interval: Duration = 25.milliseconds
161
var initialDelay: Duration = Duration.ZERO
162
var listener: EventuallyListener = NoopEventuallyListener
163
}
164
165
/**
166
* Configuration for continually testing
167
*/
168
data class ContinuallyConfiguration<T>(
169
val duration: Duration = 1.seconds,
170
val interval: Duration = 25.milliseconds,
171
val listener: ContinuallyListener<T> = NoopContinuallyListener
172
)
173
174
/**
175
* Configuration for until testing
176
*/
177
data class UntilConfiguration(
178
val duration: Duration = 5.seconds,
179
val interval: Duration = 25.milliseconds,
180
val listener: UntilListener = NoopUntilListener
181
)
182
```
183
184
**Configuration Usage:**
185
186
```kotlin
187
import io.kotest.assertions.nondeterministic.*
188
189
// Create configuration using builder
190
val config = eventuallyConfig {
191
duration = 60.seconds // Total timeout
192
interval = 500.milliseconds // Time between attempts
193
initialDelay = 2.seconds // Wait before first attempt
194
listener = { attempt, error ->
195
println("Attempt $attempt failed: ${error.message}")
196
}
197
}
198
199
eventually(config) {
200
externalApi.getData() shouldBe expectedData
201
}
202
```
203
204
### Event Listeners
205
206
Monitor retry attempts and failures during nondeterministic testing.
207
208
```kotlin { .api }
209
/**
210
* Listener for eventually test attempts and failures
211
*/
212
typealias EventuallyListener = suspend (Int, Throwable) -> Unit
213
214
/**
215
* Listener for continually test executions
216
*/
217
typealias ContinuallyListener<T> = suspend (Int, T) -> Unit
218
219
/**
220
* Listener for until test attempts and failures
221
*/
222
typealias UntilListener = suspend (Int, Throwable) -> Unit
223
224
/**
225
* No-op implementation that ignores all events
226
*/
227
object NoopEventuallyListener : EventuallyListener {
228
override suspend fun invoke(attempt: Int, error: Throwable) {}
229
}
230
```
231
232
### Interval Functions
233
234
Advanced retry strategies with customizable backoff patterns.
235
236
```kotlin { .api }
237
/**
238
* Interface for generating intervals between retry attempts
239
*/
240
interface DurationFn {
241
fun next(iteration: Int): Duration
242
}
243
244
/**
245
* Fibonacci backoff strategy with maximum duration limit
246
*/
247
class FibonacciIntervalFn(private val max: Duration) : DurationFn {
248
override fun next(iteration: Int): Duration
249
}
250
251
/**
252
* Exponential backoff strategy
253
*/
254
class ExponentialIntervalFn(
255
private val base: Duration = 25.milliseconds,
256
private val factor: Double = 2.0,
257
private val max: Duration = 1.seconds
258
) : DurationFn {
259
override fun next(iteration: Int): Duration
260
}
261
262
/**
263
* Create a fibonacci backoff function with maximum duration
264
* @receiver Base duration for calculations
265
* @param max Maximum duration to cap the backoff
266
* @return FibonacciIntervalFn instance
267
*/
268
fun Duration.fibonacci(max: Duration): FibonacciIntervalFn
269
```
270
271
**Advanced Usage Examples:**
272
273
```kotlin
274
import io.kotest.assertions.nondeterministic.*
275
276
// Using fibonacci backoff
277
val fibConfig = eventuallyConfig {
278
duration = 2.minutes
279
interval = FibonacciIntervalFn(10.seconds)
280
}
281
282
// Using exponential backoff
283
val expConfig = eventuallyConfig {
284
duration = 1.minutes
285
interval = ExponentialIntervalFn(
286
base = 100.milliseconds,
287
factor = 1.5,
288
max = 5.seconds
289
)
290
}
291
292
// Monitor retry attempts
293
val monitoredConfig = eventuallyConfig {
294
duration = 30.seconds
295
listener = { attempt, error ->
296
logger.warn("Retry attempt $attempt failed", error)
297
if (attempt > 10) {
298
metrics.increment("high_retry_count")
299
}
300
}
301
}
302
```
303
304
## Error Handling
305
306
Nondeterministic testing functions provide detailed error information:
307
308
- **Timeout errors**: Include total duration, attempt count, and last failure
309
- **Listener errors**: Exceptions in listeners don't affect test execution
310
- **Configuration validation**: Invalid durations or intervals cause immediate failure
311
- **Thread safety**: All functions are thread-safe and work with coroutines
312
313
All functions throw `AssertionError` when the final attempt fails, preserving the original assertion failure message.