2016-01-20 3 views
9

Je veux écrire des tests d'intégration pour un service qui exécute slick, puis nettoyer une base de données postgresql par la suite en annulant une transaction, mais je ne vois pas de moyen de le faire. Je comprends que je peux tester des objets DBIO qui ont été composés ensemble et les restaurer, mais il ne semble pas possible si je veux tester à un niveau d'abstraction plus élevé.Comment annuler un test d'intégration avec Slick 3 + Specs2?

En pseudocode, je veux faire:

StartDbTransaction() // setup 
DoSomethingInDB() 
AssertSomething() 
RollBackDbTransaction() // teardown 

Par exemple, si j'ai (simplifié du play-silhouette-slick-seed):

class PasswordInfoDAO(db: JdbcBackend#DatabaseDef) { 

    // ... 
    def remove(loginInfo: LoginInfo): Future[Unit] = 
     db.run(passwordInfoSubQuery(loginInfo).delete).map(_ =>()) 

} 

Je pensais que je pourrais écrire un trait PourChaque le long les lignes du Specs2 Guide, qui donne à ce un exemple générique:

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

Donc, pour marée noire, j'ai essayé ceci:

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 

} 

Alors je comptais l'utiliser un peu comme ceci:

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

Le problème est que lisse 3 ignorera ma session (par la conception) et plutôt utiliser un pool de sessions, donc mon roll-back ne fait rien. Je pense que Slick s'attend à ce que vous l'utilisiez au niveau de DBIOActions qui peut être composé ensemble et éventuellement exécuté dans des contextes différents. Slick 2 avait un moyen de contrôler la session avec .withSession, mais il a été supprimé.

La seule option pour créer, migrer et supprimer une base de données de test avec chaque test?

Répondre

5

Voici une réponse partielle. Il semble impossible ou du moins très déconseillé d'annuler une transaction en se tournant vers JDBC. J'ai donc réécrit les dépôts pour retourner les DBIO au lieu de mes objets métier. C'est l'opération de liaison monadique DBIO qui prend en charge la logique de transaction, ce qui est la seule façon de récupérer quelque chose.

class MyRepository { 

    def add(whatever: String): dbio.DBIOAction[Int, NoStream, Write with Write] = { 
     // return a DBIOAction 
    } 
} 

J'ai une fonction qui lie une action arbitraire à une exception "faux", puis renvoie le résultat futur de l'action initiale et rejette l'exception:

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

}

Donc, je peux l'utiliser à partir d'une spécification:

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

Je suis sûr qu'il y a aussi un moyen d'écrire ce mo re proprement pour specs2 comme un trait ForEach ou quelque chose de similaire.

Je pris ces idées de this et this