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

3

8

I'm trying to figure out a way to have async before and after statements where the next test cases aren't run until the completion of the action inside of the test case. In my case, it is the creating and dropping a table inside of a database

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

Is there a simple way I can remove these Await calls inside of my before and after functions?

Amorist answered 10/9, 2016 at 1:52 Comment(1)
Question: what would be the point of making these asynchronous? If I'm understanding this correctly, you won't be able to run either the before or after method concurrently with any test or with each other. So even if you find some way of avoiding Await, under the hood the system is going to have to wait until the database operation has completed before it can do anything else, or?Buote
S
5

Unfortunately, @Jeffrey Chung's solution hanged for me (since futureValue actually awaits internally). This is what I ended up doing:

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)
}
Slideaction answered 19/12, 2019 at 14:18 Comment(0)
L
4

The following is the testing approach that Dennis Vriend takes in his slick-3.2.0-test project.

First, define a dropCreateSchema method. This method attempts to create a table; if that attempt fails (because, for example, the table already exists), it drops, then creates, the 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))
    }
}

Second, define a createEntries method that populates the table with some sample data for use in each test case:

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

Third, define an initialize method that calls the above two methods sequentially:

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

In the test class, mix in the ScalaFutures trait. For example:

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

  // ...
}

Also in the test class, define an implicit conversion from a Future to a Try, and override the beforeEach method to call 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()
}

With the above pieces in place, there is no need for Await.

Longwood answered 25/8, 2018 at 15:3 Comment(0)
T
-1

You can simplify @Jeffrey Chung

A simplified dropCreateSchema method:

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

Also in the test class, I simplified beforeEach method that calls initialize. I removed an implicit conversion from a Future to a Try, and use onComplete callback:

  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()
  }
Triplett answered 19/5, 2019 at 12:18 Comment(1)
onComplete doesn't ensure that the future finishes before beforeEach returns. You have a race condition.Overexert

© 2022 - 2024 — McMap. All rights reserved.