Testing Future Objects with ScalaTest

Scala provides nice and clean ways of dealing with Future objects, like Functional Composition and For Comprehensions. Future objects are a great way of writing concurrent and parallel programs, as they allow you to execute asynchronous code and to extract the result of it at some point in the future.

However, this type of software requires a different mindset in terms of reasoning about it, be it while writing the main code or while thinking about testing it. This article will focus primarily in testing this type of code and, for this, we’ll be looking into ScalaTest, one of the best and most popular testing frameworks in the Scala ecosystem (If you want to take a look at running integration tests with Docker/Docker-compose and Makefiles, take a look at this post).

Getting started

Before we start to reason about testing Future objects, we obviously need to have some code that creates a Future object for us. We’ll start with the code below:

https://gist.github.com/lucmolinari/a068736e76d763c1fc138a80fd2d030b#file-helloworldasyncspec-scala

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object HelloWorld {

  val theOneWhoCannotBeNamed = "Voldemort"

  def sayHelloTo(name: String): Future[Option[String]] = {
    Future {
      if (theOneWhoCannotBeNamed.equals(name)) None else Some(getHelloMessage(name))
    }
  }

  def getHelloMessage(name: String): String = {
    s"Hello ${name}, welcome to the future world!"
  }

}

The code is pretty straightforward and provides a function that receives a name and returns an Option[String] with a message wrapped in a Future object. If the input name is “The One Who Cannot Be Named”, then it just returns None, otherwise a welcome message is returned. It’s important to remember that in order to create a Future, you need to inform an ExecutionContext, and that can be done either explicitly or implicitly. In our case, we chose the latter option, by using the implicit ExecutionContext provided by scala.concurrent.ExecutionContext.Implicits.global.

back_to_the_future

ScalaTest

ScalaTest is a testing framework for the Scala ecosystem that provides a lot of nice features and flexibility. One cool thing is that it allows you to write your tests in a variety of different styles, such as TDD and BDD. Take a look here to see them all.

There are a few ways we can test our HelloWorld object.

Await.result

The easiest way would be to use Await.result, as seen below:

import org.scalatest.{FunSuite, Matchers}

import scala.concurrent.Await
import scala.concurrent.duration._

class HelloWorldSpecWithAwait extends FunSuite with Matchers {

  test("A valid message should be returned to a valid name") {
    val result = Await.result(HelloWorld.sayHelloTo("Harry"), 500 millis)
    result shouldBe Some("Hello Harry, welcome to the future world!")
  }

  test("No message should be returned to the one who cannot be named") {
    val result = Await.result(HelloWorld.sayHelloTo("Voldemort"), 500 millis)
    result shouldBe None
  }

}

With this approach, we explicitly wait until the Future is ready and then assert its result. While this works, it doesn’t look very clean, so let’s look into other approaches.

ScalaFutures

ScalaTest provides a trait called ScalaFutures, that allows you to easily test Future objects.

import org.scalatest.concurrent.ScalaFutures
import org.scalatest.{FunSuite, Matchers}

class HelloWorldSpecWithScalaFutures extends FunSuite with Matchers with ScalaFutures {

  test("A valid message should be returned to a valid name") {
    whenReady(HelloWorld.sayHelloTo("Harry")) { result =>
      result shouldBe Some("Hello Harry, welcome to the future world!")
    }
  }

  test("No message should be returned to the one who cannot be named") {
    whenReady(HelloWorld.sayHelloTo("Voldemort")) { result =>
      result shouldBe None
    }
  }

}

The whenReady function receives a Future and wait until it’s completed, then you can extract its result and use it in any type of assertion you want to.

Internally, whenReady has 2 configurations:

  • timeout: Maximum time to wait until the Future is completed. If it’s not completed within this time, a TestFailedException will be thrown. Its default value is 150ms.
  • interval: The interval used to keep pooling the Future object to know if it’s been completed or not. Its default value is 15ms.

When the default values are not enough, they can be overridden by 2 ways:

Globally

You can create an implicit value of type PatienceConfig and that will be applied to all calls of whenReady:

import org.scalatest.time._
...

implicit override val patienceConfig = PatienceConfig(timeout = Span(2, Seconds), interval = Span(20, Millis))

Individually for each whenReady call

If you prefer to define these configurations individually for each whenReady invocation, you can mix in SpanSugar to get a concise DSL and then define the timeout and interval:

import org.scalatest.concurrent.ScalaFutures
import org.scalatest.time._
import org.scalatest.{FunSuite, Matchers}

class HelloWorldSpecWithScalaFutures extends FunSuite with Matchers with ScalaFutures with SpanSugar {

  test("A valid message should be returned to a valid name") {
    whenReady(HelloWorld.sayHelloTo("Harry"), timeout(2 seconds), interval(500 millis)) { result =>
      result shouldBe Some("Hello Harry, welcome to the future world!")
    }
  }
  ...
}

This approach gives you more fine grained control, in case you need it. Of course, you can define a global configuration and just use specific timeouts where needed.

Asynchronous Test Suites

If you are using ScalaTest 3.x, a new set of test suites were added. For each one of the existing suites (FunSuite, WordSpec, etc), there’s a corresponding asynchronous version (AsyncFunSpec, AsyncWordSpec, etc). These suites take a different approach to test your Future objects, where each test returns a Future[Assertion] and the assertions are just performed when the Future is completed. Everything is non-blocking, as opposed to whenReady, that blocks waiting until the Future object is completed.

import org.scalatest.{AsyncFunSuite, Matchers}

class HelloWorldAsyncSpec extends AsyncFunSuite with Matchers {

  test("A valid message should be returned to a valid name") {
    HelloWorld.sayHelloTo("Harry") map { result =>
      result shouldBe Some("Hello Harry, welcome to the future world!")
    }
  }

  test("No message should be returned to the one who cannot be named") {
    HelloWorld.sayHelloTo("Voldemort") map { result =>
      result shouldBe None
    }
  }

}

Conclusion

The goal of this article was to provide some insights of how to test Future objects in Scala using ScalaTest, which is a quite complete and flexible framework that also provides a very good documentation.

If you want to take a look at running integration tests with Docker/Docker-compose and Makefiles, take a look at this post.

Leave a comment