2016-01-20 13 views
9

Ich möchte einige Integrationstests für einen Dienst schreiben, der glatt läuft, und anschließend eine postgresql-Datenbank bereinigen, indem ich eine Transaktion zurückbringe, aber ich sehe keinen Weg, dies zu tun. Ich verstehe, dass ich zusammengesetzte DBIO-Objekte testen und zurückrollen kann, aber es sieht nicht so aus, als ob es möglich ist, auf einer höheren Abstraktionsebene zu testen.Wie kann ich einen Integrationstest mit Slick 3 + Specs2 rückgängig machen?

In Pseudo-Code, möchte ich dies tun:

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

Zum Beispiel, wenn ich dies (aus dem play-silhouette-slick-seed vereinfacht):

class PasswordInfoDAO(db: JdbcBackend#DatabaseDef) { 

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

} 

Ich dachte, ich entlang einer foreach Charakterzug schreiben konnte die Zeilen der Specs2 Guide, die dies ein generisches Beispiel gibt:

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

Also für Slick, habe ich versucht, dies:

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 

} 

Dann wollte ich es benutzen Art wie folgt aus:

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

Das Problem ist, dass Slick 3 meine Session (mit Absicht) ignoriert und stattdessen ein Sitzungspool, sodass mein Roll-Back nichts bewirkt. Ich denke, dass Slick erwartet, dass Sie es auf der Ebene von DBIOActions verwenden sollten, die zusammen komponiert und möglicherweise in verschiedenen Kontexten ausgeführt werden können. Slick 2 hatte eine Möglichkeit, die Sitzung mit .withSession zu steuern, aber es wurde entfernt.

Ist die einzige Option zum Erstellen, Migrieren und Löschen einer Testdatenbank mit jedem Test?

Antwort

5

Hier ist eine Teilantwort. Es scheint entweder unmöglich oder zumindest sehr unwägbar zu sein, eine Transaktion rückgängig zu machen, indem man auf JDBC zurückgreift. Stattdessen habe ich die Repositorys neu geschrieben, um DBIOs anstelle meiner Geschäftsobjekte zurückzugeben. Es ist die monadische DBIO-Bindeoperation, die sich um die Transaktionslogik kümmert, also ist dies wirklich die einzige Möglichkeit, etwas zurückzurollen.

class MyRepository { 

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

Ich habe eine Funktion, die eine beliebige Aktion auf eine „fake“ Ausnahme bindet, und gibt dann die Zukunft aufgrund der ursprünglichen Handlung und verwirft die Ausnahme:

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

}

Dann kann ich das aus einer Spezifikation verwenden:

Ich bin sicher, es gibt auch eine Möglichkeit, diese mo zu schreiben Richtig für Specs2 als ForEach-Eigenschaft oder etwas Ähnliches.

nahm ich diese Ideen von this und this