2016-01-20 14 views
9

Desidero scrivere alcuni test di integrazione per un servizio che esegue slick e quindi pulire un database postgresql in seguito ripristinando una transazione, ma non vedo un modo per farlo. Capisco che posso testare gli oggetti DBIO che sono stati composti insieme e rotoli indietro, ma non sembra possibile se voglio testare a un livello più alto di astrazione.Come posso eseguire il rollback di un test di integrazione con Slick 3 + Specs2?

In pseudocodice, voglio fare questo:

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

Per esempio, se ho questo (semplificato dal play-silhouette-slick-seed):

class PasswordInfoDAO(db: JdbcBackend#DatabaseDef) { 

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

} 

ho pensato che avrei potuto scrivere un tratto ForEach lungo le linee del Specs2 Guide, che fornisce un esempio generico:

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

Così, per slick, ho provato questo:

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 

} 

Poi ho pensato di usarlo un po 'come questo:

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

Il problema è che liscia 3 sarà ignorare la mia sessione (in base alla progettazione) e di utilizzare invece un pool di sessioni, quindi il mio rollback non fa nulla. Penso che Slick abbia l'aspettativa che dovresti usarlo a livello di DBIOActions che può essere composto insieme ed eventualmente eseguito in diversi contesti. Slick 2 aveva un modo per controllare la sessione con .withSession, ma è stata rimossa.

È l'unica opzione per creare, migrare e rilasciare un database di test con ogni test?

risposta

5

Ecco una risposta parziale. Sembra impossibile o almeno molto sconsigliabile eseguire il rollback di una transazione raggiungendo il JDBC. Così, invece, ho riscritto i repository per restituire i DBIO invece dei miei oggetti di business. È l'operazione di bind monadico DBIO che si occupa della logica delle transazioni, quindi questo è davvero l'unico modo per eseguire il rollback di qualcosa.

class MyRepository { 

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

Ho una funzione che lega l'azione arbitraria di un'eccezione di "falso", e poi restituisce il risultato futuro dell'azione originale e scarta l'eccezione:

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

}

allora posso usare questo da una specifica:

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

sono sicuro che ci sia anche un modo per scrivere questo mo ri pulito per specs2 come tratto ForEach o qualcosa di simile.

ho preso queste idee da this e this

Problemi correlati