如何确保给定的未来在测试中首先完成?

Posted

技术标签:

【中文标题】如何确保给定的未来在测试中首先完成?【英文标题】:How to make sure a given future completes first in tests? 【发布时间】:2021-06-20 06:27:27 【问题描述】:

我正在为函数 bar 编写测试:

def bar(fut1: Future[Int], 
        fut2: Future[Int], 
        fut3: Future[Int]): Future[Result] = ???

bar 像这样返回Result

 case class Result(
    x: Int,          // fut1 value
    oy: Option[Int], // if fut2 is complete then Some of fut2 value else None 
    oz: Option[Int]  // if fut3 is complete then Some of fut3 value else None 
 ) 

我想为所有测试用例编写测试:

fut1 已完成,fut2fut3 未完成 fut1 完成,fut2 完成,fut3 未完成 等

所以我正在为这些测试编写函数foo1foo2foo3 实现。

def foo1(x: Int): Future[Int] = ??? 
def foo2(x: Int): Future[Int] = ??? 
def foo3(x: Int): Future[Int] = ??? 

Test #1 调用所有这些函数,检查 fut1 是否先完成,然后调用 bar

val fut1 = foo1(0)
val fut2 = foo2(0)
val fut3 = foo3(0)

// make sure `fut1` completes first

测试#2 调用所有这些函数,确保fut2 首先完成,然后调用bar。 测试#3 调用所有这些函数,确保fut3 首先完成,然后调用bar

我的问题是如何实现函数foo1foo2foo3以及测试。

【问题讨论】:

如果它们是异步的,为什么它们完成的顺序很重要? @ViktorKlang 顺便说一句,非常好的问题,在尝试找到答案之前我应该​​自己问一下。 @ViktorKlang 这对测试很重要。我想测试三个不同的用例。想象一下,我正在为 firstCompletedOf 编写测试。 @ViktorKlang 我更新了问题并意识到您是对的,异步操作的顺序并不重要。谢谢。 不客气,@Michael 【参考方案1】:

您可以尝试通过map 将完整性时间戳附加到每个未来,例如:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent._
import scala.concurrent.duration._
import scala.language.postfixOps

def foo1(x: Int): Future[Int] = Future Thread.sleep(200); 1 
def foo2(x: Int): Future[Int] = Future Thread.sleep(500); 2 
def foo3(x: Int): Future[Int] = Future Thread.sleep(500); 3

def completeTs[T](future: Future[T]): Future[(T, Long)] = future.map(v => v -> System.currentTimeMillis())

val resutls = Await.result(Future.sequence(
  List(completeTs(foo1(1)), completeTs(foo2(1)), completeTs(foo3(1)))
), 2 seconds)

val firstCompleteTs = resutls.map(_._2).min
val firstCompleteIndex = resutls.indexWhere(_._2 == firstCompleteTs)
assert(firstCompleteIndex == 0)

斯卡蒂:https://scastie.scala-lang.org/L9g78DSNQIm2K1jGlQzXBg

【讨论】:

【参考方案2】:

您可以重新利用firstCompletedOf 来验证期货列表中给定指数的期货是否是第一个完成的指数:

import java.util.concurrent.atomic.AtomicReference
import scala.concurrent.ExecutionContext, Future, Promise
import scala.util.Try

def isFirstCompleted[T](idx: Int)(futures: List[Future[T]])(
    implicit ec: ExecutionContext): Future[Boolean] = 
  val promise = Promise[(T, Int)]()
  val pRef = new AtomicReference[Promise[(T, Int)]](promise)
  futures.zipWithIndex foreach  case (f, i) => f onComplete  case tt: Try[T] =>
      val p = pRef.getAndSet(null)
      if (p != null) p tryComplete tt.map((_, i))
    
  
  promise.future.map case (t, i) => i == idx 

试运行:

import scala.concurrent.ExecutionContext.Implicits.global

val futures = List(
  FutureThread.sleep(100); 1,
  FutureThread.sleep(150); throw new Exception("oops!"),
  FutureThread.sleep(50); 3
)

isFirstCompleted(0)(futures)  // Future(Success(false))
isFirstCompleted(2)(futures)  // Future(Success(true))

要编写测试用例,请考虑使用ScalaTest AsyncFlatSpec

【讨论】:

【参考方案3】:

目前尚不清楚您要测试的究竟是什么。 如果你只是使用已经完成的期货,你会得到你描述的行为:

def f1 = Future.successful(1)
def f2 = Future.successful(2)
def f3 = Future.successful(3)

eventually 
   Future.firstCompletedOf(Seq(f1, f2, f3)).value shouldBe Some(1)

(请注意,您不能像您在问题中所做的那样直接与fut1 进行比较,这总是错误的,因为.firstCompletedOf 返回一个新的未来)。

你也可以只完成一个未来,而不要管其他的:

    val promise = Promise[Int].future
    def f1 = promise.future // or just Future.successful(1) ... or Future(1)
    def f2 = Future.never
    def f3 = Future.never
  
    result =  Future.firstCompletedOf(Seq(f1, f2, f3))
    promise.complete(Success(1)) 
    eventually 
       result.value shouldBe 1
    

等等......也可以让其他期货也得到他们自己的承诺的支持,例如,如果你希望它们最终都完成(不确定它会给你带来什么,但话又说回来,我不确定你正在这里开始测试)。

另一种可能是让它们相互依赖:

    val promise = Promise[Int]
    def f1 = promise.future
    def f2 = promise.future.map(_ + 1)
    def f3 = promise.future.map(_ + 2)
    
    ... 
    promise.complete(Success(1))

【讨论】:

非常感谢!这个答案非常有用。你是对的,不清楚我在测试什么。我会更新问题。 我更新了问题并意识到您的第二个建议确实回答了它。顺序并不重要。我可以用Future.successfulFuture.never 伪造foo1foo2foo3

以上是关于如何确保给定的未来在测试中首先完成?的主要内容,如果未能解决你的问题,请参考以下文章

论:关于自动化测试的前期发展历史及未来发展趋势

FxCop 规则确保在测试中首先调用某个接受 lambda 的方法

如何编写预期的失败?

实体框架迁移 - 如何创建单元测试以确保迁移模型是最新的?

如何在颤振小部件测试中捕获来自未来的错误?

你如何使用WaitGroup确保goroutine在for循环中完成?