如何使用 Slick 3 + Specs2 回滚集成测试?
How can I roll back an integration test with Slick 3 + Specs2?
我想为运行流畅的服务编写一些集成测试,然后通过回滚事务来清理 postgresql 数据库,但我没有找到这样做的方法。我知道我可以测试组合在一起的 DBIO 对象并将它们回滚,但如果我想在更高的抽象级别上进行测试,这似乎是不可能的。
在伪代码中,我想这样做:
StartDbTransaction() // setup
DoSomethingInDB()
AssertSomething()
RollBackDbTransaction() // teardown
例如,如果我有这个(从 play-silhouette-slick-seed 简化而来):
class PasswordInfoDAO(db: JdbcBackend#DatabaseDef) {
// ...
def remove(loginInfo: LoginInfo): Future[Unit] =
db.run(passwordInfoSubQuery(loginInfo).delete).map(_ => ())
}
我想我可以按照 Specs2 Guide 的方式编写 ForEach 特征,这给出了一个通用示例:
// a transaction with the database
trait Transaction
trait DatabaseContext extends ForEach[Transaction] {
// you need to define the "foreach" method
def foreach[R: AsResult](f: Transaction => R): Result = {
val transaction = openDatabaseTransaction
try AsResult(f(transaction))
finally closeDatabaseTransaction(transaction)
}
// create and close a transaction
def openDatabaseTransaction: Transaction = ???
def closeDatabaseTransaction(t: Transaction) = ???
}
class FixtureSpecification extends mutable.Specification with DatabaseContext {
"example 1" >> { t: Transaction =>
println("use the transaction")
ok
}
"example 2" >> { t: Transaction =>
println("use it here as well")
ok
}
}
所以为了光滑,我试过这个:
override def foreach[R: AsResult](f: JdbcBackend#DatabaseDef => R): Result = {
val db = dbConfig.db
val session = db.createSession()
session.conn.setAutoCommit(false)
val result = AsResult(f(db))
session.conn.rollback()
result
}
然后我打算像这样使用它:
class PasswordInfoDAOSpec(implicit ee: ExecutionEnv)
extends Specification with DatabaseContext {
"password" should {
"be removed from db" in { db =>
// arrange
db.run(...) // something to set up the database
// act
PasswordInfoDAO(db).remove(loginInfo).await
// assert
PasswordInfoDAO(db).find(loginInfo) must be None.await
}
}
}
问题是 slick 3 会忽略我的会话(按设计),而是使用会话池,所以我的回滚不会做任何事情。我认为 Slick 期望您应该在 DBIOActions 级别使用它,它可以组合在一起并可能在不同的上下文中执行。 Slick 2 有一种方法可以用 .withSession
控制会话,但它已被删除。
是为每个测试创建、迁移和删除测试数据库的唯一选项吗?
这是部分答案。通过向下达到 JDBC 来回滚事务似乎是不可能的,或者至少是非常不可取的。因此,我将存储库重写为 return DBIO 而不是我的业务对象。负责事务逻辑的是 DBIO monadic 绑定操作,因此这确实是回滚某些内容的唯一方法。
class MyRepository {
def add(whatever: String): dbio.DBIOAction[Int, NoStream, Write with Write] = {
// return a DBIOAction
}
}
我有一个函数可以将任意操作绑定到 "fake" 异常,然后 return 原始操作的 Future 结果并丢弃异常:
case class IntentionalRollbackException[R](successResult: R) extends Exception("Rolling back transaction")
def runWithRollback[R, S <: slick.dbio.NoStream, E <: slick.dbio.Effect](action: DBIOAction[R, S, E]): Future[R] = {
val block = action.flatMap(r => DBIO.failed(new IntentionalRollbackException(r)))
val tryResult = dbConfig.db.run(block.transactionally.asTry)
// not sure how to eliminate these casts from Any
tryResult.map {
case Failure(IntentionalRollbackException(successResult)) => successResult.asInstanceOf[R]
case Failure(t) => throw t
case Success(r) => r.asInstanceOf[R]
}
}
那么我可以从规范中使用它:
val insertAction1 = new MyRepository().add("whatever 1").withPinnedSession
val insertAction2 = new MyRepository().add("whatever 2").withPinnedSession
val actions = insertAction1 andThen insertAction2
val result = Await.result(runWithRollback(action), 5.seconds)
result must be ...
我敢肯定还有一种方法可以更清楚地为 specs2 将其编写为 ForEach 特征或类似的东西。
您必须将所有逻辑放入 DBIO,然后包含一个 DBIO.failed 步骤来执行回滚。请参阅 https://github.com/slick/slick/commit/6caaea3a8a888d54dc51463bc0e1725191b9721a,添加 3.2 的 "rollback" 文档(但 IIUC 从 3.0 开始它是正确的)
我在谷歌搜索如何在 Slick 中回滚后来到这个页面,最终发现 Slick 3 提供了对底层 JDBC 连接的访问,允许它在不需要使 DBIO 失败的情况下完成:
val rollback: DBIO[Unit] = SimpleDBIO(_.connection.rollback)
def runAndRollback[A](dbio: DBIO[A]): DBIO[A] = dbio.andFinally(rollback).transactionally
我想为运行流畅的服务编写一些集成测试,然后通过回滚事务来清理 postgresql 数据库,但我没有找到这样做的方法。我知道我可以测试组合在一起的 DBIO 对象并将它们回滚,但如果我想在更高的抽象级别上进行测试,这似乎是不可能的。
在伪代码中,我想这样做:
StartDbTransaction() // setup
DoSomethingInDB()
AssertSomething()
RollBackDbTransaction() // teardown
例如,如果我有这个(从 play-silhouette-slick-seed 简化而来):
class PasswordInfoDAO(db: JdbcBackend#DatabaseDef) {
// ...
def remove(loginInfo: LoginInfo): Future[Unit] =
db.run(passwordInfoSubQuery(loginInfo).delete).map(_ => ())
}
我想我可以按照 Specs2 Guide 的方式编写 ForEach 特征,这给出了一个通用示例:
// a transaction with the database
trait Transaction
trait DatabaseContext extends ForEach[Transaction] {
// you need to define the "foreach" method
def foreach[R: AsResult](f: Transaction => R): Result = {
val transaction = openDatabaseTransaction
try AsResult(f(transaction))
finally closeDatabaseTransaction(transaction)
}
// create and close a transaction
def openDatabaseTransaction: Transaction = ???
def closeDatabaseTransaction(t: Transaction) = ???
}
class FixtureSpecification extends mutable.Specification with DatabaseContext {
"example 1" >> { t: Transaction =>
println("use the transaction")
ok
}
"example 2" >> { t: Transaction =>
println("use it here as well")
ok
}
}
所以为了光滑,我试过这个:
override def foreach[R: AsResult](f: JdbcBackend#DatabaseDef => R): Result = {
val db = dbConfig.db
val session = db.createSession()
session.conn.setAutoCommit(false)
val result = AsResult(f(db))
session.conn.rollback()
result
}
然后我打算像这样使用它:
class PasswordInfoDAOSpec(implicit ee: ExecutionEnv)
extends Specification with DatabaseContext {
"password" should {
"be removed from db" in { db =>
// arrange
db.run(...) // something to set up the database
// act
PasswordInfoDAO(db).remove(loginInfo).await
// assert
PasswordInfoDAO(db).find(loginInfo) must be None.await
}
}
}
问题是 slick 3 会忽略我的会话(按设计),而是使用会话池,所以我的回滚不会做任何事情。我认为 Slick 期望您应该在 DBIOActions 级别使用它,它可以组合在一起并可能在不同的上下文中执行。 Slick 2 有一种方法可以用 .withSession
控制会话,但它已被删除。
是为每个测试创建、迁移和删除测试数据库的唯一选项吗?
这是部分答案。通过向下达到 JDBC 来回滚事务似乎是不可能的,或者至少是非常不可取的。因此,我将存储库重写为 return DBIO 而不是我的业务对象。负责事务逻辑的是 DBIO monadic 绑定操作,因此这确实是回滚某些内容的唯一方法。
class MyRepository {
def add(whatever: String): dbio.DBIOAction[Int, NoStream, Write with Write] = {
// return a DBIOAction
}
}
我有一个函数可以将任意操作绑定到 "fake" 异常,然后 return 原始操作的 Future 结果并丢弃异常:
case class IntentionalRollbackException[R](successResult: R) extends Exception("Rolling back transaction")
def runWithRollback[R, S <: slick.dbio.NoStream, E <: slick.dbio.Effect](action: DBIOAction[R, S, E]): Future[R] = {
val block = action.flatMap(r => DBIO.failed(new IntentionalRollbackException(r)))
val tryResult = dbConfig.db.run(block.transactionally.asTry)
// not sure how to eliminate these casts from Any
tryResult.map {
case Failure(IntentionalRollbackException(successResult)) => successResult.asInstanceOf[R]
case Failure(t) => throw t
case Success(r) => r.asInstanceOf[R]
}
}
那么我可以从规范中使用它:
val insertAction1 = new MyRepository().add("whatever 1").withPinnedSession
val insertAction2 = new MyRepository().add("whatever 2").withPinnedSession
val actions = insertAction1 andThen insertAction2
val result = Await.result(runWithRollback(action), 5.seconds)
result must be ...
我敢肯定还有一种方法可以更清楚地为 specs2 将其编写为 ForEach 特征或类似的东西。
您必须将所有逻辑放入 DBIO,然后包含一个 DBIO.failed 步骤来执行回滚。请参阅 https://github.com/slick/slick/commit/6caaea3a8a888d54dc51463bc0e1725191b9721a,添加 3.2 的 "rollback" 文档(但 IIUC 从 3.0 开始它是正确的)
我在谷歌搜索如何在 Slick 中回滚后来到这个页面,最终发现 Slick 3 提供了对底层 JDBC 连接的访问,允许它在不需要使 DBIO 失败的情况下完成:
val rollback: DBIO[Unit] = SimpleDBIO(_.connection.rollback)
def runAndRollback[A](dbio: DBIO[A]): DBIO[A] = dbio.andFinally(rollback).transactionally