Skip to content

Commit a801069

Browse files
committed
Updated eventually for 5.7
1 parent 516ae9f commit a801069

File tree

3 files changed

+171
-299
lines changed

3 files changed

+171
-299
lines changed

documentation/versioned_docs/version-5.7/assertions/eventually.md

Lines changed: 163 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -4,126 +4,214 @@ title: Eventually
44
slug: eventually.html
55
---
66

7+
:::note New improved module
8+
Starting with Kotest 5.7, the non-deterministic testing functions have moved to the `kotest-assertions-core` module, and
9+
are available under the new package `com.sksamuel.kotest.assertions.nondeterministic`. The previous iterations of these
10+
functions are still available, but deprecated.
11+
:::
712

8-
When testing non-deterministic code, a common use case is "I expect this code to pass after a short period of time".
13+
Testing non-deterministic code can be hard. You might need to juggle threads, timeouts, race conditions, and the
14+
unpredictability of when events are happening.
915

10-
For example, if you were testing a IO operation, you might need to wait until the IO operation has flushed.
16+
For example, if you were testing that an asynchronous file write was completed successfully, you need to wait until the
17+
write operation has completed and flushed to disk.
1118

12-
Sometimes you can do a Thread.sleep but this is isn't ideal as you need to set a sleep threshold high enough so that it
13-
won't expire prematurely on a slow machine. Plus it means that your test will sit around waiting on the timeout even if
14-
the code completes quickly on a fast machine.
19+
Some common approaches to these problems are:
1520

16-
Or you can roll a loop and sleep and retry and sleep and retry, but this is just boilerplate slowing you down.
21+
* Using callbacks which are invoked once the operation has completed. The callback can be then used to assert that the
22+
state of the system is as we expect. But not all operations provide callback functionality.
1723

18-
Another common approach is to use countdown latches and this works fine if you are able to inject the latches in the
19-
appropriate places but it isn't always possible to have the code under test trigger a latch.
24+
* Block the thread using `Thread.sleep` or suspend a function using `delay`, waiting for the operation to complete.
25+
The sleep threshold needs to be set high enough to be sure the operations will have completed on a fast or slow
26+
machine. Plus it means that your test will sit around waiting on the timeout even if
27+
the code completes quickly on a fast machine.
2028

21-
As an alternative, kotest provides the `eventually` function and the `Eventually` configuration which periodically test
22-
the code ignoring your specified exceptions and ensuring the result satisfies an optional predicate, until the timeout
23-
is eventually reached or too many iterations have passed. This is flexible and is perfect for testing nondeterministic
24-
code.
29+
* Use a loop with a sleep and retry and a sleep and retry, but then you need to write boilerplate to track number of
30+
iterations, handle certain exceptions and fail on others, ensure the total time taken has not exceeded the max and so
31+
on.
2532

33+
* Use countdown latches and block threads until the latches are released by the non-determistic operation. This can
34+
work well if you are able to inject the latches in the appropriate places, but just like callbacks, it isn't always
35+
possible to have the code to be tested integrate with a latch.
2636

27-
### Examples
37+
As an alternative to the above solutions, kotest provides the `eventually` function which solves the common use case of
38+
_"**I expect this code to pass after a short period of time**"_.
2839

40+
Eventually works by periodically invoking a given lambda, ignoring specified exceptions, until the lambda passes, or a
41+
timeout is reached, or too many
42+
iterations have passed. This is flexible and is perfect for testing nondeterministic code. Eventually can be customized
43+
with regards to the types of exceptions to handle, how the lambda is considered a success or failure, with a listener,
44+
and so on.
2945

30-
#### Simple examples
46+
## API
3147

32-
Let's assume that we send a message to an asynchronous service.
33-
After the message is processed, a new row is inserted into user table.
48+
There are two ways to use eventually. The first is simply providing a duration, using the Kotlin `Duration` type,
49+
followed by the code that should eventually pass without an exception being raised.
3450

35-
We can check this behaviour with our `eventually` function.
51+
For example:
3652

3753
```kotlin
38-
class MyTests : ShouldSpec() {
39-
init {
40-
should("check if user repository has one row after message is sent") {
41-
sendMessage()
42-
eventually(5.seconds) {
43-
userRepository.size() shouldBe 1
44-
}
45-
}
46-
}
54+
eventually(5.seconds) {
55+
userRepository.getById(1).name shouldBe "bob"
4756
}
4857
```
4958

50-
#### Exceptions
59+
The second is by providing a config block. This method should be used when you need to
60+
set more options than just the duration. It also allows the config to be shared between multiple invocations of
61+
eventually.
62+
63+
For example:
64+
65+
```kotlin
66+
val config = eventuallyConfig {
67+
duration = 1.seconds
68+
interval = 100.milliseconds
69+
}
5170

52-
By default, `eventually` will ignore any `AssertionError` that is thrown inside the function (note, that means it won't catch `Error`).
53-
If you want to be more specific, you can tell `eventually` to ignore specific exceptions and any others will immediately fail the test.
71+
eventually(config) {
72+
userRepository.getById(1).name shouldBe "bob"
73+
}
74+
```
5475

55-
Let's assume that our example from before throws a `UserNotFoundException` while the user is not found in the database.
56-
It will eventually return the user when the message is processed by the system.
76+
## Configuration Options
5777

58-
In this scenario, we can explicitly skip the exception that we expect to happen until the test passed, but any other exceptions would
59-
not be ignored. Note, this example is similar to the former, but if there was some other error, say a ConnectionException for example, this would cause
60-
the eventually block to immediately exit with a failure message.
78+
### Durations and Intervals
6179

80+
The duration is the total amount of time to keep trying to pass the test. The `interval` allows us to
81+
specify how often the test should be attempted. So if we set duration to 5 seconds, and interval to 250 millis,
82+
then the test would be attempted at most `5000 / 250 = 20` times.
6283

6384
```kotlin
64-
class MyTests : ShouldSpec() {
65-
init {
66-
should("check if user repository has one row") {
67-
eventually(5.seconds, UserNotFoundException::class.java) {
68-
userRepository.findBy(1) shouldNotBe null
69-
}
70-
}
71-
}
85+
val config = eventuallyConfig {
86+
duration = 5.seconds
87+
interval = 250.milliseconds
7288
}
7389
```
7490

91+
Alternatively, rather than specifying the interval as a fixed number, we can pass in a function. This allows us to
92+
perform some kind of backoff, or anything else we need.
7593

94+
For example, to use a fibonacci increasing interval, starting with 100ms:
7695

77-
#### Predicates
96+
```kotlin
97+
val config = eventuallyConfig {
98+
duration = 5.seconds
99+
intervalFn = 100.milliseconds.fibonacci()
100+
}
101+
```
102+
103+
### Initial Delay
78104

79-
In addition to verifying a test case eventually runs without throwing, we can also verify the result and treat a non-throwing result as failing.
105+
Usually `eventually` starts executing the test block immediately, but we can add an initial delay before the first
106+
iteration using `initialDelay`, such as:
80107

81108
```kotlin
82-
class MyTests : StringSpec({
83-
"check that predicate eventually succeeds in time" {
84-
var i = 0
85-
eventually<Int>(25.seconds, predicate = { it == 5 }) {
86-
delay(1.seconds)
87-
i++
88-
}
89-
}
90-
})
109+
val config = eventuallyConfig {
110+
initialDelay = 1.seconds
111+
}
91112
```
92113

93-
#### Sharing configuration
114+
### Retries
94115

95-
Sharing the configuration for eventually is a breeze with the `Eventually` data class. Suppose you have classified the operations in your
96-
system to "slow" and "fast" operations. Instead of remembering which timing values were for slow and fast we can set up some objects to share between tests
97-
and customize them per suite. This is also a perfect time to show off the listener capabilities of `eventually` which give you insight
98-
into the current value of the result of your producer and the state of iterations!
116+
In addition to bounding the number of invocations by time, we can do so by iteration count. In the following example
117+
we retry the operation 10 times, or until 8 seconds has expired.
99118

100119
```kotlin
101-
val slow = EventuallyConfig<ServerResponse, ServerException>(5.minutes, interval = 25.milliseconds.fibonacci(), exceptionClass = ServerException::class)
102-
val fast = slow.copy(duration = 5.seconds)
120+
val config = eventuallyConfig {
121+
initialDelay = 8.seconds
122+
retries = 10
123+
}
124+
125+
eventually(config) {
126+
userRepository.getById(1).name shouldBe "bob"
127+
}
128+
```
129+
130+
### Specifying the exceptions to trap
131+
132+
By default, `eventually` will ignore any `AssertionError` that is thrown inside the function (note, that means it won't
133+
catch `Error`). If you want to be more specific, you can tell `eventually` to ignore specific exceptions and any others
134+
will immediately fail the test. We call these exceptions, the _expected exceptions_.
135+
136+
For example, when testing that a user should exist in the database, a `UserNotFoundException` might be thrown
137+
if the user does not exist. We know that eventually that user will exist. But if an `IOException` is thrown, we don't
138+
want to keep retrying as this indicates a larger issue than simply timing.
103139

104-
class FooTests : StringSpec({
105-
val logger = logger("FooTests")
106-
val fSlow = slow.copy(listener = { i, t -> logger.info("Current $i after {${t.times} attempts")})
140+
We can do this by specifying that `UserNotFoundException` is an exception to suppress.
141+
142+
```kotlin
143+
val config = eventuallyConfig {
144+
duration = 5.seconds
145+
expectedExceptions = setOf(UserNotFoundException::class)
146+
}
107147

108-
"server eventually provides a result for /foo" {
109-
eventually(fSlow) {
148+
eventually(config) {
149+
userRepository.getById(1).name shouldBe "bob"
150+
}
151+
```
152+
153+
As an alternative to passing in a set of exceptions, we can provide a function which is invoked, passing in the throw
154+
exception. This function should return true if the exception should be ignored, or false if the exception should bubble
155+
out.
156+
157+
```kotlin
158+
val config = eventuallyConfig {
159+
duration = 5.seconds
160+
expectedExceptions = { it is UserNotFoundException }
161+
}
162+
163+
eventually(config) {
164+
userRepository.getById(1).name shouldBe "bob"
165+
}
166+
```
167+
168+
### Listeners
169+
170+
We can attach a listener, which will be invoked on each iteration, with the current iteration count and the
171+
exception that caused the iteration to fail. Note: The listener will not be fired on a successful invocation.
172+
173+
```kotlin
174+
val config = eventuallyConfig {
175+
duration = 5.seconds
176+
listener = { k, throwable -> println("Iteration $k, with cause $throwable") }
177+
}
178+
179+
eventually(config) {
180+
userRepository.getById(1).name shouldBe "bob"
181+
}
182+
```
183+
184+
### Sharing configuration
185+
186+
Sharing the configuration for eventually is a breeze with the `eventuallyConfig` builder.
187+
Suppose you have classified the operations in your system to "slow" and "fast" operations. Instead of remembering
188+
which timing values were for slow and
189+
fast we can set up some objects to share between tests and customize them per suite. This is also a perfect time to show
190+
off the listener capabilities of `eventually` which give you insight into the current value of the result of your
191+
producer and the state of iterations!
192+
193+
```kotlin
194+
val slow = eventuallyConfig {
195+
duration = 5.minutes
196+
interval = 25.milliseconds.fibonacci()
197+
listener = { i, t -> logger.info("Current $i after {${t.times} attempts") }
198+
}
199+
200+
val fast = slow.copy(duration = 5.seconds)
201+
202+
class FooTests : FunSpec({
203+
test("server eventually provides a result for /foo") {
204+
eventually(slow) {
110205
fooApi()
111206
}
112207
}
113208
})
114209

115-
class BarTests : StringSpec({
116-
val logger = logger("BarTests")
117-
val bFast = fast.copy(listener = { i, t -> logger.info("Current $i after {${t.times} attempts")})
118-
119-
"server eventually provides a result for /bar" {
120-
eventually(bFast) {
210+
class BarTests : FunSpec({
211+
test("server eventually provides a result for /bar") {
212+
eventually(fast) {
121213
barApi()
122214
}
123215
}
124216
})
125-
126217
```
127-
128-
Here we can see sharing of configuration can be useful to reduce duplicate code while allowing flexibility for things like
129-
custom logging per test suite for clear test logs.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy