如何测试返回 Future 的方法?

21

我想测试一个返回Future的方法。我的尝试如下:

import  org.specs2.mutable.Specification
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}

class AsyncWebClientSpec extends Specification{

  "WebClient when downloading images" should {
    "for a valid link return non-zero content " in {
      val testImage = AsyncWebClient.get("https://www.google.cz/images/srpr/logo11ww.png")
      testImage.onComplete { res => 
        res match {
          case Success(image) => image must not have length(0)
          case _ =>
        }
        AsyncWebClient.shutDown
      }
    }
  }
}

除了我无法使这段代码工作之外,我认为测试 futures 应该有更好的方法,并使用 Future 匹配器进行测试。

在 specs2 中应该如何正确实现呢?

6个回答

21
你可以使用Matcher.await方法将Matcher[T]转换为Matcher[Future[T]]:
val testImage: Future[String] =
   AsyncWebClient.get("https://www.google.cz/images/srpr/logo11ww.png")  

// you must specify size[String] here to help type inference
testImage must not have size[String](0).await

// you can also specify a number of retries and duration between retries
testImage must not have size[String](0).await(retries = 2, timeout = 2.seconds)

// you might also want to check exceptions in case of a failure
testImage must throwAn[Exception].await

6
我需要进行特殊的导入才能使它工作吗?我正在使用specs2 3.6.2版本,但是出现了以下错误: value await is not a member of org.specs2.matcher.BeEqualTo [error] Future(1) must beEqualTo(1).await - Yar
3
是的,你需要一个隐式的执行环境 - Eric

14

花了我一些时间才找到这个,所以想分享一下。我应该先读一下发布说明。在specs2 v3.5中,使用隐式ExecutionEnv来使用await for future是必需的。这也可以用于future转换(即map),请参见http://notes.implicit.ly/post/116619383574/specs2-3-5

以下为快速参考的摘录:

import org.specs2.concurrent.ExecutionEnv

class MySpec extends mutable.Specification {
  "test of a Scala Future" >> { implicit ee: ExecutionEnv =>
    Future(1) must be_>(0).await
  }
}

1
你也可以将它添加到类签名中 case class MyFutureSpec(implicit ee: ExecutionEnv) extends Specification - nmat

11

等待是一种反模式。不应该使用它。 您可以使用像ScalaFuturesIntegrationPatienceEventually等特性。

whenReady会完成您要寻找的魔术。

例:

import org.specs2.mutable.Specification
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}
import scala.concurrent.Future

class AsyncWebClientSpec extends Specification
    with ScalaFutures
    with IntegrationPatience {

    "WebClient when downloading images" should {

        "for a valid link return non-zero content " in {

            whenReady(Future.successful("Done")){ testImage =>

                testImage must be equalTo "Done"           
                // Do whatever you need
            }

        }
    }
}

1
这应该是一个被接受的答案。whenReady 似乎是最干净的解决方案。 - Branislav Lazic

7

在 specs2 中有一个很好的东西 - 针对 Future[Result] 的隐式 await 方法。如果你利用 future 转换,你可以这样写:

"save notification" in {
  notificationDao.saveNotification(notification) map { writeResult =>
    writeResult.ok must be equalTo (true)
  } await
}

未来的组合在需要使用异步函数进行数据排列时派上了用场:
"get user notifications" in {
  {
    for {
      _ <- notificationDao.saveNotifications(user1Notifications)
      _ <- notificationDao.saveNotifications(user2Notifications)
      foundUser1Notifications <- notificationDao.getNotifications(user1)
    } yield {
      foundUser1Notifications must be equalTo (user1Notifications)
    }
  } await
}

请注意,我们必须在for循环周围使用额外的块来说服编译器。我认为这很嘈杂,所以如果我们将await方法转换为函数,我们可以得到更好的语法:
def awaiting[T]: Future[MatchResult[T]] => Result = { _.await }

"get user notifications" in awaiting {
  for {
    _ <- notificationDao.saveNotifications(user1Notifications)
    _ <- notificationDao.saveNotifications(user2Notifications)
    foundUser1Notifications <- notificationDao.getNotifications(user1)
  } yield {
    foundUser1Notifications must be equalTo (user1Notifications)
  }
}

1
你能否添加完整的代码片段并在一个完整的测试类中使用它吗?我得到了“await”不可用的错误。尝试修复它,但是该死的隐式导入让人很难弄对。 - James
@James 我发现没有使用 await 的顶部示例对我有效。 - Loic Lacomme

4

想知道为什么@etorreborre没有提到"最终"

请查看https://github.com/etorreborre/specs2/blob/master/tests/src/test/scala/org/specs2/matcher/EventuallyMatchersSpec.scala#L10-L43

class EventuallyMatchersSpec extends Specification with FutureMatchers with ExpectationsDescription { section("travis")
addParagraph { """
`eventually` can be used to retry any matcher until a maximum number of times is reached
or until it succeeds.
""" }

  "A matcher can match right away with eventually" in {
    1 must eventually(be_==(1))
  }
  "A matcher can match right away with eventually, even if negated" in {
    "1" must not (beNull.eventually)
  }
  "A matcher will be retried automatically until it matches" in {
    val iterator = List(1, 2, 3).iterator
    iterator.next must be_==(3).eventually
  }
  "A matcher can work with eventually and be_== but a type annotation is necessary or a be_=== matcher" in {
    val option: Option[Int] = Some(3)
    option must be_==(Some(3)).eventually
  }

请参阅 https://github.com/etorreborre/specs2/blob/master/tests/src/test/scala/org/specs2/matcher/FutureMatchersSpec.scala#L14-L16。 - SemanticBeeng
3
这是因为最终只会重试一个值 =>T,这个值在重新调用时可能会改变(例如 iterator.next 的例子),而不是像等待 Future 一样需要等待。 - Eric

3

onComplete返回的是Unit,这意味着代码块会立即返回并且测试在能够执行任何操作之前便已结束。为了正确地测试Future的结果,需要阻塞直到它完成。您可以使用Await来实现,同时设置最大Duration等待时间。

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

Await.result(testImage, Duration("10 seconds")) must not have length(0)

我也考虑过这个问题,但是对我来说不清楚的是,由于Await.Result返回T,那么我该如何测试失败场景。 - jaksky
虽然我不认为在断言中设置时间限制是一个好主意,但它仍然能工作 :) - prayagupa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接