ScalaTest:在失败的期货中断言异常(非阻塞)

Posted

技术标签:

【中文标题】ScalaTest:在失败的期货中断言异常(非阻塞)【英文标题】:ScalaTest: Assert exceptions in failed futures (non-blocking) 【发布时间】:2014-01-22 09:41:43 【问题描述】:
import org.scalatest. FlatSpec, Matchers, ParallelTestExecution 
import org.scalatest.concurrent.ScalaFutures
import org.apache.thrift.TApplicationException

class Test extends FlatSpec with Matchers with ScalaFutures with ParallelTestExecution 
  it should "throw org.apache.thrift.TApplicationException for invalid Ids" in 
    val future: Future[Response] = ThriftClient.thriftRequest
    whenReady(future) 
      res => 
       intercept[TApplicationException] 
       
      
    
  

问题:如何在 Futures 中断言预期的失败而不阻塞?以上不起作用,在intercept块之前抛出异常。

【问题讨论】:

【参考方案1】:

我知道这可能有点晚了,但是 ScalaTest 通过混合 ScalaFutures 特征或直接在您的测试函数中使用它,开箱即用地提供了这个功能(我相信从版本 2 开始)。看!

test("some test") 
  val f: Future[Something] = someObject.giveMeAFuture
  ScalaFutures.whenReady(f.failed)  e =>
    e shouldBe a [SomeExceptionType]
  

或者您可以在其中执行一些其他断言。基本上,如果你的未来没有像你预期的那样失败,那么测试就会失败。如果失败,但抛出了不同的异常,则测试将失败。好,易于! =]


厚颜无耻的编辑:

你也可以使用这个方法来测试任何返回未来的东西:

test("some test") 
  val f: Future[Something] = someObject.giveMeAFuture
  ScalaFutures.whenReady(f)  s =>
    // run assertions against the object returned in the future
  


最近的编辑!

我只是想根据更新版本的 Scala 测试用更多有用的信息来更新这个答案。各种规格特征现在都具有异步支持,因此不要扩展 WordSpec,而是扩展 AsyncWordSpec,而不是像上面那样依赖 whenReady 调用,你只需直接映射你的期货在测试中。

例子:

class SomeSpec extends Async[*]Spec with Matchers 

...

  test("some test") 
    someObject.funcThatReturnsAFutureOfSomething map  something =>
      // run assertions against the 'something' returned in the future
    
  

【讨论】:

好方法,+1。接受的答案更有用,因为我们在内部使用 Twitter Futures。 @flavian:为什么这不适用于 Scala 和 Twitter 期货? @TimoReimann 因为 Twitter 期货首先是一种不同的类型。我听到 Twitter 谈论让它们扩展 scala.conccurrent.Future 但据我所知,这还没有完成。此问题的答案已包含在我们的 util 库中,可在此处公开获取:github.com/websudos/util。 不,不晚!很高兴您提交了这个答案。 ScalaTest DSL 需要一些时间来适应。 啊,那个小failed 投影是诀窍,我试过whenReady 没有成功,直到你指出来,谢谢。我认为它的表现力稍差,但如果你想简洁,这也可以:f.failed.futureValue shouldBe a [SomeExceptionType].【参考方案2】:

这也隐藏在评论中,但 Scalatest 的 FutureValues mixin 已经涵盖了您。

只需使用f.failed.futureValue shouldBe an[TApplicationException]

【讨论】:

这确实很好,但我之所以关心内部结构是因为有问题的代码实际上是基于 Twitter 期货而不是 Scala 期货。 有趣。看起来像一个“TwitterFutures”mixin 如果它们不能与 twitter 期货一起使用,那么它会是一个很好的 ScalaTest 插件。【参考方案3】:

注意:留下这个答案是因为 OP 发现它很有帮助,但对于 Scala Futures,请参阅另一个答案。

这有点样板,但Waiter来自AsyncAssertions

import org.scalatest. FlatSpec, Matchers, ParallelTestExecution 
import org.scalatest.concurrent. ScalaFutures, AsyncAssertions, PatienceConfiguration 
import concurrent.Future
import concurrent.ExecutionContext.Implicits._
import util._ 

class Test extends FlatSpec with Matchers with ScalaFutures with ParallelTestExecution with AsyncAssertions 
  it should "throw for invalid Ids" in 
    val f: Future[Int] = new Goof().goof
    val w = new Waiter
    f onComplete 
      case Failure(e) => w(throw e); w.dismiss()
      case Success(_) => w.dismiss()
    
    intercept[UnsupportedOperationException] 
      w.await
    
  

给定

import concurrent.Future
import concurrent.ExecutionContext.Implicits._

class Goof 
  def goof(delay: Int = 1): Future[Int] = Future 
    Thread sleep delay * 1000L
    throw new UnsupportedOperationException
   
  def goofy(delay: Int = 1): Future[Int] = Future 
    Thread sleep delay * 1000L
    throw new NullPointerException
   
  def foog(delay: Int = 1): Future[Int] = Future 
    Thread sleep delay * 1000L
    7
  

换句话说,

class Test extends FlatSpec with Matchers with ScalaFutures with ParallelTestExecution with AsyncAssertions 
  it should "throw for invalid Ids" in 
    val f: Future[Int] = new Goof().goof
    import Helper._
    f.failing[UnsupportedOperationException] 
  


object Helper 
  implicit class Failing[A](val f: Future[A]) extends Assertions with AsyncAssertions 
    def failing[T <: Throwable](implicit m: Manifest[T]) = 
      val w = new Waiter
      f onComplete 
        case Failure(e) => w(throw e); w.dismiss()
        case Success(_) => w.dismiss()
      
      intercept[T] 
        w.await
      
     
   
 

或者,如果您有多个期货,并且您希望第一个不合格的期货未能通过测试:

trait FailHelper extends Assertions with AsyncAssertions with PatienceConfiguration 
  def failingWith[T <: Throwable : Manifest](fs: Future[_]*)(implicit p: PatienceConfig) 
    val count = new java.util.concurrent.atomic.AtomicInteger(fs.size)
    val w = new Waiter
    for (f <- fs) f onComplete 
      case Success(i) =>
        w(intercept[T](i))
        println(s"Bad success $i")
        w.dismiss()
      case Failure(e: T) =>
        println(s"Failed $e OK, count $count.get")
        w(intercept[T](throw e))
        if (count.decrementAndGet == 0) w.dismiss()
      case Failure(e) =>
        println(s"Failed $e Bad")
        w(intercept[T](throw e))
        w.dismiss()
    
    w.await()(p)
  

有用法

class Test extends FlatSpec with Matchers with ScalaFutures with ParallelTestExecution with FailHelper 
  it should "throw for invalid Ids" in 
    val sut = new Goof()
    import sut._

    val patienceConfig = null  // shadow the implicit
    implicit val p = PatienceConfig(timeout = 10 seconds)

    // all should fail this way
    //failingWith[UnsupportedOperationException](goof(), goofy(3), foog(5))
    //failingWith[UnsupportedOperationException](goof(), foog(5))
    failingWith[UnsupportedOperationException](goof(), goof(2), goof(3))
  

灵感来自this unloved answer。

【讨论】:

【参考方案4】:

ScalaTest 3.0 添加async versions of the spec traits 和AsyncFreeSpec 一样:

import org.scalatest.AsyncFlatSpec, Matchers
import scala.concurrent.Future

class ScratchSpec extends AsyncFlatSpec with Matchers  

    def thriftRequest = Future  throw new Exception() 

    it should "throw exception" in 
        recoverToSucceededIf[Exception] 
            thriftRequest
        
    

【讨论】:

【参考方案5】:

除了Brian Low 的回答,我还为recoverToSucceededIf 找到了一个很好的解释。这适用于所有 Async 样式(来自 ScalaTest 3):

可以通过两种方式测试失败的期货:使用recoverToSucceededIfrecoverToExceptionIf

recoverToSucceededIf 用于断言未来结束的异常类型:
"return UserNotFoundException" when 
       "the user does not exist" in 
         recoverToSucceededIf[UserNotFoundException](userService.findUser("1"))
       
     
recoverToExceptionIf 在您想要测试某些异常字段时很有用:
"return UserAlreadyExistsException" when 
     "adding a user with existing username" in 
       recoverToExceptionIf[UserAlreadyExistsException] 
         userService.addUser(user)
       .map  ex =>
         ex.message shouldBe s"User with username: $username already exists!"
       
     
    

查看Tudor Zgureanu — What's new in ScalaTest 3的整个博客

【讨论】:

【参考方案6】:

你也可以试试这个简单而简短的东西

test("some test throwing SQL Exception") 
      val f: Future[Something] = someObject.giveMeAFuture
      recoverToSucceededIf[SQLException](f)
    

【讨论】:

以上是关于ScalaTest:在失败的期货中断言异常(非阻塞)的主要内容,如果未能解决你的问题,请参考以下文章

在 ScalaTest 中使用“不应该产生 [异常]”语法

如何以非阻塞方式链接期货?也就是说,如何在不阻塞的情况下将一个future用作另一个future的输入?

用 ScalaTest 比较集合内容

如何在 ScalaTest 中显示自定义失败消息?

调试断言失败:C# 应用程序中的 C++ 异常?

小部件库捕获的异常。 (断言失败)