在 scalatest 中创建和删除 scala slick 表之前和之后的异步

Async before and after for creating and dropping scala slick tables in scalatest

我正在尝试找出一种方法来使用异步 beforeafter 语句,其中下一个测试用例 运行 直到完成内部的操作测试用例。在我的例子中,它是在数据库

中创建和删除 table
  val table = TableQuery[BlockHeaderTable]
  val dbConfig: DatabaseConfig[PostgresDriver] = DatabaseConfig.forConfig("databaseUrl")
  val database: Database = dbConfig.db
  before {
    //Awaits need to be used to make sure this is fully executed before the next test case starts
    //TODO: Figure out a way to make this asynchronous 
    Await.result(database.run(table.schema.create), 10.seconds)
  }

  "BlockHeaderDAO" must "store a blockheader in the database, then read it from the database" in {
    //...
  }

  it must "delete a block header in the database" in {
    //...
  }

  after {
    //Awaits need to be used to make sure this is fully executed before the next test case starts
    //TODO: Figure out a way to make this asynchronous
    Await.result(database.run(table.schema.drop),10.seconds)
  }

有没有一种简单的方法可以删除 beforeafter 函数中的这些 Await 调用?

以下是 Dennis Vriend 在他的 slick-3.2.0-test 项目中采用的测试方法。

首先,定义一个dropCreateSchema方法。此方法尝试创建一个 table;如果该尝试失败(例如,因为 table 已经存在),它会删除,然后创建 table:

def dropCreateSchema: Future[Unit] = {
  val schema = BlockHeaderTable.schema
  db.run(schema.create)
    .recoverWith {
      case t: Throwable =>
        db.run(DBIO.seq(schema.drop, schema.create))
    }
}

其次,定义一个 createEntries 方法,用一些样本数据填充 table 以供每个测试用例使用:

def createEntries: Future[Unit] = {
  val setup = DBIO.seq(
    // insert some rows
    BlockHeaderTable ++= Seq(
      BlockHeaderTableRow(/* ... */),
      // ...
    )
  ).transactionally
  db.run(setup)
}

三、定义一个initialize方法依次调用上面两个方法:

def initialize: Future[Unit] = for {
  _ <- dropCreateSchema
  _ <- createEntries
} yield ()

在测试 class 中,混入 ScalaFutures 特征。例如:

class TestSpec extends FlatSpec
  with Matchers
  with ScalaFutures
  with BeforeAndAfterAll
  with BeforeAndAfterEach {

  // ...
}

同样在测试 class 中,定义从 FutureTry 的隐式转换,并覆盖 beforeEach 方法以调用 initialize :

implicit val timeout: Timeout = 10.seconds

implicit class PimpedFuture[T](self: Future[T]) {
  def toTry: Try[T] = Try(self.futureValue)
}

override protected def beforeEach(): Unit = {
  blockHeaderRepo.initialize // in this example, initialize is defined in a repo class
    .toTry recover {
      case t: Throwable =>
        log.error("Could not initialize the database", t)
    } should be a 'success
}

override protected def afterAll(): Unit = {
  db.close()
}

有了上面的部分,就不需要Await

你可以简化@Jeffrey Chung

简化的dropCreateSchema方法:

def dropCreateSchema: Future[Unit] = {
  val schema = users.schema
  db.run(DBIO.seq(schema.dropIfExists, schema.create))
}

同样在测试 class 中,我简化了调用 initializebeforeEach 方法。我删除了从 FutureTry 的隐式转换,并使用 onComplete 回调:

  override protected def beforeEach(): Unit = {
    initialize.onComplete(f => 
      f recover {
        case t: Throwable =>
          log.error("Could not initialize the database", t)
      } should be a 'success)
  }

  override protected def afterAll(): Unit = {
    db.close()
  }

不幸的是,@Jeffrey Chung 的解决方案对我来说是悬而未决的(因为 futureValue 实际上在内部等待)。这就是我最终做的:

import org.scalatest.{AsyncFreeSpec, FutureOutcome}
import scala.concurrent.Future

class TestTest extends AsyncFreeSpec /* Could be any AsyncSpec. */ {
  // Do whatever setup you need here.
  def setup(): Future[_] = ???
  // Cleanup whatever you need here.
  def tearDown(): Future[_] = ???
  override def withFixture(test: NoArgAsyncTest) = new FutureOutcome(for {
    _ <- setup()
    result <- super.withFixture(test).toFuture
    _ <- tearDown()
  } yield result)
}