
Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: December 2, 2024
ScalaTest is one of the most popular and widely used testing frameworks in Scala. It offers various tools to write expressive and concise tests, and one of its standout features is the eventually block. This utility is especially useful when dealing with asynchronous or eventually consistent systems, such as microservices, distributed databases, or message queues.
In this tutorial, let’s look at how to use the eventually block effectively.
First, let’s add the ScalaTest library dependency to our build.sbt file:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % Test
Now, let’s create a simple test. For this example, we’ll simulate an asynchronous operation where a value is updated after a delay. This pattern resembles scenarios such as database write operations, where updates take some time to complete:
class AsyncOperation(waitTime: Int) {
private var database: Map[String, String] = Map.empty
def writeToDatabase(key: String, value: String): Unit = {
new Thread(() => {
Thread.sleep(waitTime) // Simulate a delay in writing to the database
database += (key -> value)
}).start()
}
def readFromDatabase(key: String): Option[String] = database.get(key)
}
The writeToDatabase() method simulates a delayed async write, while readFromDatabase() checks the result. Let’s write a simple test to verify this code:
it should "get the result from the database after save" in {
val op = AsyncOperation(100)
op.writeToDatabase("key", "value")
op.readFromDatabase("key") shouldBe Some("value")
}
This test fails because the read operation is attempted before the write operation has finished.
Using ScalaTest’s eventually block, we’ll verify that the data appears in the database after the write operation is completed.
The eventually block in ScalaTest repeatedly retries the provided code until the assertion passes or the specified timeout is reached. This is particularly useful for testing asynchronous operations, eventual consistency, or delayed responses, such as database writes, API calls, or message queue processing.
We can incorporate the eventually block by either extending the test class with the Eventually trait or importing it directly. In this example, we’ll mix in the Eventually trait to our test class:
class EventuallyUnitTest extends AnyFlatSpec with Matchers with Eventually {
it should "retry the save to database multiple times in eventually with default timeout" in {
val op = AsyncOperation(100)
op.writeToDatabase("key", "value")
eventually {
op.readFromDatabase("key") shouldBe Some("value")
}
}
}
This code is similar to the failed test described in the previous section but with one difference: the read operation is now wrapped within the eventually block. As a result, the eventually block retries the read operation multiple times until the assertion succeeds or the timeout is reached. By default, it uses a timeout of 150 milliseconds and a retry interval of 15 milliseconds. Since the write operation takes 100 milliseconds, the default timeout provides sufficient time for the read operation to succeed, allowing the test to pass.
Additionally, the eventually block uses a backoff algorithm to optimize retries. This helps to avoid overloading the system by gradually increasing the interval between each retry.
Let’s see what happens if the write operation takes more than 150 milliseconds. Let’s update the test to use more time for write operation:
val op = AsyncOperation(300)
op.writeToDatabase("key", "value")
eventually {
op.readFromDatabase("key") shouldBe Some("value")
}
Running the test with this code fails, displaying the message:
The code passed to eventually never returned normally.
Attempted 5 times over 154.83845499999998 milliseconds.
Last failure message: None was not equal to Some("value").
This shows that even after 5 retries over approximately 150 milliseconds, the read operation didn’t return the expected value. It’s important to note that due to task scheduling and thread availability, the actual timeout may slightly exceed the configured 150 milliseconds.
In cases where we know the operation requires more time, we can adjust the timeout by providing a custom implicit value of type PatienceConfig using the given keyword, which the eventually block will use:
given patienceConfig: PatienceConfig =
PatienceConfig(
timeout = scaled(Span(2, Seconds)),
interval = scaled(Span(5, Millis))
)
This configuration sets custom values for the timeout and interval. Using scaled with the new defaults is optional but recommended, as it allows the values to adjust dynamically when tests run on systems with varying performance. With this updated configuration, the test now passes as the timeout is increased to 2 seconds.
Additionally, instead of using the implicit configuration, we can also pass the timeout and interval explicitly in the eventually block:
eventually(timeout = Timeout(Span(2, Seconds)), interval = Interval(Span(50, Millis))) {
op.readFromDatabase("new-key") shouldBe Some("value")
}
This makes the timeout clearer than just using an implicit variable.
In this article, we explored the eventually block in ScalaTest and its usefulness in verifying asynchronous operations, particularly in integration tests. We also discussed how to configure timeout values to meet specific requirements.