mbakhiet
mbakhiet

Reputation: 35

Unit Testing failures from Futures in Scala

I'm trying to test the error handling in a script I'm writing. If the async function fetchBar fails I pattern match the failure case and then return a successful future containing the failed result.

val fetchedBar = Try(fooClient.fetchBar(params))

fetchedBar match {
  case Success(bar) => foobar(bar)
  case Failure(e) => Future.successful(FooResult(success = false))
}

However when I unit test this flow I'm having trouble testing the failure case. I have stubbed fetchBar to return a failed future as shown below.

val fetchedBar = Try(Future.failed(new Exception()))

But I noticed that fetchedBar returns a Success rather than a Failure. Why is that and how can I stub the fetchBar function to create a failed Try?

Upvotes: 1

Views: 2027

Answers (1)

J0HN
J0HN

Reputation: 26961

I think you're slightly mixing concepts - but that's not 100% your fault.

The thing is, Future in Scala is a bit non-orthogonal concept - that it is, it represents not only the notion of delayed execution, but also a notion of failure.

Because of this, in most cases it doesn't make much sense to wrap the Future into the Try, or vice versa - unless one wants to explicitly separate the notion of failure from the notion of asynchrony.

In other words, the following combinations are sort of weird, but still has their use:

  1. Try[Future[_]] - future already captures failures. However, makes sense if you have a (bad-behaving) library method that normally returns a Future, but may throw on a "synchronous" path:
def futureReciprocal(i: Int): Float = { 
   val reciprocal = 1 / i // Division by zero is intentional
   Future.successful(reciprocal)
}

futureReciprocal(0) // throws
Try(futureReciprocal(0)) // Failure(DivisionByZero(...))

... but this is basically a workaround to fix poorly implemented function

  1. Future[Try[_]] - sometimes useful to separate the "business" error (represented by Future.success(Failure(...))) from "infrastructure" failure (represented by Future.failed(...)). On a side - this is especially useful with akka-streams, which tend to treat failed futures as "fatal" to the stream.

In your case, what you want to do is to assert on the result of the future. To do so, you have at least two options, actually.

  1. Block till the future is completed and check the outcome - this is usually done with scala.concurrent.Await:
// writing this without the compiler, might mix up namespaces a bit
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt

val future = fooClient.fetchBar(...)
val futureResult: Try[_] = Await.result(future, 1.second)
futureResult match { case Success(_) => ??? ; case Failure(exc) => ???; }
  1. Use some test framework that supports working with futures - e.g. scalatest:
class YourTest extends FlatSpec with ScalaFutures {
   "fetchBar should return failed future" in {
        val future: Future[XYZ] = fooClient.fetchBar(...)
        // whenReady comes from the ScalaFutures trait
        whenReady(future) { result => result shouldBe XYZ } // asserting on the successful future result
        whenReady(future.failed) { exc => exc shoulBe a[RuntimeException] } // asserting on an exception in the failed future
   }
}

Upvotes: 3

Related Questions