Подтвердить что ты не робот

ScalaTest: Исключить ошибки в неудачных фьючерсах (неблокирующих)

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.

4b9b3361

Ответ 1

Примечание: оставляя этот ответ, потому что 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))
  }
}

Вдохновленный этот нелюбимый ответ.

Ответ 2

Я знаю, что это, вероятно, немного поздно, но ScalaTest предоставляет эту функцию из коробки (я полагаю, начиная с версии 2), смешивая черту ScalaFutures или используя ее непосредственно в ваших тестовых функциях. Вот!

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

Ответ 3

Это тоже было похоронено в комментарии, но Scalatest FutureValues ​​mixin вы охватили.

Просто используйте f.failed.futureValue shouldBe an[TApplicationException]

Ответ 4

ScalaTest 3.0 добавляет асинхронные версии спецификаций spec, например 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

Вы также можете попробовать это что-то простое и короткое

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

Ответ 6

Помимо ответа Брайана Лоу, я нашел хорошее объяснение для recoverToSucceededIf. Это доступно во всех стилях Async (из ScalaTest 3):

Неудачные фьючерсы могут быть протестированы двумя способами: с помощью recoverToSucceededIf или recoverToExceptionIf

  • 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!"
       }
     }
   } 

Посмотреть весь блог от Тудора Згуряну - Что нового в ScalaTest 3