【问题标题】:ScalaTest: Assert exceptions in failed futures (non-blocking)ScalaTest:在失败的期货中断言异常(非阻塞)
【发布时间】: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块之前抛出异常。

【问题讨论】:

    标签: scala scalatest


    【解决方案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 traitsAsyncFreeSpec 一样:

      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)
              }
          

          【讨论】:

            猜你喜欢
            • 2014-11-16
            • 2019-03-11
            • 2011-09-16
            • 1970-01-01
            • 2013-12-10
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多