2017-07-07 1 views
0

Ich habe eine Menge Probleme beim Erstellen eines Anwendungstests und hoffte, dass jemand mit etwas mehr Scala-Erfahrung mir in die richtige Richtung zeigen könnte.Probleme mit der Slick/Postgres-Datenbankprüfung in Play2/Specs2

Ich habe eine Reihe von Datenmodellen, die sich in einer Postgres-Datenbank befinden und mit Slick Case-Klassen zugeordnet sind. Die My Play-App stellt dann JSON-basierte REST-Endpunkte für diese Datenmodelle bereit. Da der Großteil des tatsächlichen Codes zwischen jedem Endpunkt ähnlich ist, wird der Großteil des Codes als Merkmal implementiert, das in die eigentlichen Controller gemischt wird, die die erforderlichen Bits übersteuern.

Dies funktioniert gut, aber wenn ich versuche, Unit-Tests auf jeden von ihnen laufen die meisten Controller arbeiten dann habe ich am Ende mit dem Fehler:

[error] Can't find a constructor for class helpers.DatabaseHelper 
[warn] c.z.h.HikariConfig - The jdbcConnectionTest property is now deprecated, see the documentation for connectionTestQuery 
[error] 
[error] cannot create an instance for class FileControllerSpec 
[error] caused by java.sql.SQLTransientConnectionException: db - Connection is not available, request timed out after 1005ms. 
[error] caused by org.postgresql.util.PSQLException: FATAL: remaining connection slots are reserved for non-replication superuser connections 
[error] 
[error] STACKTRACE 
[error] sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) 
[error] sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) 
[error] sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) 
[error] java.lang.reflect.Constructor.newInstance(Constructor.java:423) 
[error] org.specs2.reflect.Classes$$anonfun$org$specs2$reflect$Classes$$createInstanceForConstructor$1.apply(Classes.scala:104) 
[error] org.specs2.control.ActionT$$anonfun$safe$1.apply(ActionT.scala:88) 
[error] org.specs2.control.ActionT$$anonfun$reader$1$$anonfun$apply$6.apply(ActionT.scala:79) 
[error] org.specs2.control.Status$.safe(Status.scala:100) 
[error] org.specs2.control.StatusT$$anonfun$safe$1.apply(StatusT.scala:62) 
[error] org.specs2.control.StatusT$$anonfun$safe$1.apply(StatusT.scala:62) 
[error] scalaz.syntax.ToApplicativeOps$$anon$1.self$lzycompute(ApplicativeSyntax.scala:29) 
[error] scalaz.syntax.ToApplicativeOps$$anon$1.self(ApplicativeSyntax.scala:29) 
[error] scalaz.syntax.ToApplicativeOps$ApplicativeIdV$$anonfun$point$1.apply(ApplicativeSyntax.scala:33) 
[error] scalaz.WriterTApplicative$$anonfun$point$1.apply(WriterT.scala:282) 
[error] scalaz.WriterTApplicative$$anonfun$point$1.apply(WriterT.scala:282) 
[error] scalaz.effect.IO$$anonfun$apply$19$$anonfun$apply$20.apply(IO.scala:136) 
[error] scalaz.effect.IO$$anonfun$apply$19$$anonfun$apply$20.apply(IO.scala:136) 
[error] scalaz.FreeFunctions$$anonfun$return_$1.apply(Free.scala:326) 
[error] scalaz.FreeFunctions$$anonfun$return_$1.apply(Free.scala:326) 
[error] scalaz.std.FunctionInstances$$anon$1$$anonfun$map$1.apply(Function.scala:56) 
[error] scalaz.Free$$anonfun$run$1.apply(Free.scala:172) 
[error] scalaz.Free$$anonfun$run$1.apply(Free.scala:172) 
[error] scalaz.Free.go2$1(Free.scala:119) 
[error] scalaz.Free.go(Free.scala:122) 
[error] scalaz.Free.run(Free.scala:172) 
[error] scalaz.effect.IO$class.unsafePerformIO(IO.scala:22) 
[error] scalaz.effect.IOFunctions$$anon$6.unsafePerformIO(IO.scala:227) 
[error] org.specs2.reflect.Classes$$anonfun$createInstance$1$$anonfun$apply$1$$anonfun$3.apply(Classes.scala:37) 
[error] org.specs2.reflect.Classes$$anonfun$createInstance$1$$anonfun$apply$1$$anonfun$3.apply(Classes.scala:36) 
[error] scala.collection.immutable.List.map(List.scala:273) 
[error] org.specs2.reflect.Classes$$anonfun$createInstance$1$$anonfun$apply$1.apply(Classes.scala:36) 
[error] org.specs2.reflect.Classes$$anonfun$createInstance$1$$anonfun$apply$1.apply(Classes.scala:29) 
[error] scala.Function1$$anonfun$andThen$1.apply(Function1.scala:52) 
[error] org.specs2.control.Status$class.fold(Status.scala:30) 
[error] org.specs2.control.Ok.fold(Status.scala:95) 
[error] org.specs2.control.Status$class.flatMap(Status.scala:48) 
[error] org.specs2.control.Ok.flatMap(Status.scala:95) 
[error] org.specs2.control.Status$class.map(Status.scala:45) 
[error] org.specs2.control.Ok.map(Status.scala:95) 
[error] org.specs2.control.StatusT$$anonfun$map$1.apply(StatusT.scala:16) 
[error] org.specs2.control.StatusT$$anonfun$map$1.apply(StatusT.scala:16) 
[error] scalaz.WriterT$$anonfun$map$1.apply(WriterT.scala:46) 
[error] scalaz.WriterT$$anonfun$map$1.apply(WriterT.scala:46) 
[error] scalaz.effect.IO$$anonfun$map$1$$anonfun$apply$8.apply(IO.scala:56) 
[error] scalaz.effect.IO$$anonfun$map$1$$anonfun$apply$8.apply(IO.scala:55) 
[error] scalaz.Free$$anonfun$map$1.apply(Free.scala:52) 
[error] scalaz.Free$$anonfun$map$1.apply(Free.scala:52) 
[error] scalaz.Free$$anonfun$flatMap$1$$anonfun$apply$1.apply(Free.scala:60) 
[error] scalaz.Free$$anonfun$flatMap$1$$anonfun$apply$1.apply(Free.scala:60) 
[error] scalaz.Free.resume(Free.scala:72) 
[error] scalaz.Free.go2$1(Free.scala:118) 
[error] scalaz.Free.go(Free.scala:122) 
[error] scalaz.Free.run(Free.scala:172) 
[error] scalaz.effect.IO$class.unsafePerformIO(IO.scala:22) 
[error] scalaz.effect.IOFunctions$$anon$6.unsafePerformIO(IO.scala:227) 
[error] org.specs2.runner.SbtRunner$$anonfun$newTask$1$$anon$4.execute(SbtRunner.scala:37) 
[error] sbt.ForkMain$Run$2.call(ForkMain.java:294) 
[error] sbt.ForkMain$Run$2.call(ForkMain.java:284) 
[error] java.util.concurrent.FutureTask.run(FutureTask.java:266) 
[error] java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) 
[error] java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) 
[error] java.lang.Thread.run(Thread.java:745) 

DatabaseHelper ist ein Objekt, das meine Testdaten einrichtet.

Durch die Reduzierung der Anzahl der Spezifikationen wird der Fehler behoben. Daher weiß ich, dass das Problem nicht bei dieser speziellen Testspezifikation liegt.

Ich verwende Runtime-DI wie von der Play-Dokumentation empfohlen und überschreite die Datenbankbindung für Tests, um zu vermeiden, meine Entwicklungsdatenbank auszublenden.

Ich benutze Play Evolutionen, um das Datenbankschema zu verwalten, aber absichtlich die Datenbank auszublenden und sie während Setup/Teardown neu einzurichten, um sicherzustellen, dass sie vor jeder Testspezifikation vollständig ist.

Ich denke, dass alle meine Testspezifikationen zur gleichen Zeit initialisiert werden, was bedeutet, dass alle gleichzeitig versuchen, sich mit der Datenbank zu verbinden, und daher keine Verbindungen mehr zur Verfügung stehen.

Ich habe versucht, die ParallelExecution und gleichzeitigeRegistrierungseinstellungen in sbt, um nur einen Prozess auf einmal zu setzen, aber dies ist ohne Erfolg. Ich habe versucht, jede Spezifikation sequenziell zu setzen, aber das scheint nicht zu funktionieren. Ich habe auch versucht, die Ausnahme zu fangen und das Setup zu versuchen, aber das scheint auch nicht zu funktionieren.

Ich weiß jetzt nicht, was ich versuchen soll, damit meine Tests funktionieren! Bitte helfen Sie.

Vielen Dank.


Testfall:

@RunWith(classOf[JUnitRunner]) 
class FileControllerSpec extends GenericControllerSpec { 
    sequential 

    override val componentName: String = "FileController" 
    override val uriRoot: String = "/file" 

    override def testParsedJsonObject(checkdata: JsLookupResult, parsed_test_json: JsValue) = { 
    val object_keys = Seq("filepath","user","ctime","mtime","atime") 
    val object_keys_int = Seq("storage","version") 

    object_keys.map(key=> 
     (checkdata \ key).as[String] must equalTo((parsed_test_json \ key).as[String]) 
    ) ++ object_keys_int.map(key=> 
     (checkdata \ key).as[Int] must equalTo((parsed_test_json \ key).as[Int]) 
    ) 
    } 

    override val testGetId: Int = 3 
    override val testGetDocument: String = """{"filepath":"/path/to/a/video.mxf","storage":1,"user":"me","version":1,"ctime":"1970-01-01T04:25:45.678+0100","mtime":"1970-01-01T04:25:45.678+0100","atime":"1970-01-01T04:25:45.678+0100"}""" 
    override val testCreateDocument: String = """{"filepath":"/path/to/some/other.project","storage":1,"user":"test","version":3,"ctime":"2017-03-17T13:51:00.123+0000","mtime":"2017-03-17T13:51:00.123+0000","atime":"2017-03-17T13:51:00.123+0000"}""" 
    override val minimumNewRecordId = 3 
    override val testDeleteId: Int = 2 
    override val testConflictId: Int = -1 
} 

GenericControllerSpec:

@RunWith(classOf[JUnitRunner]) 
trait GenericControllerSpec extends Specification with BeforeAfterAll { 
    //can over-ride bindings here. see https://www.playframework.com/documentation/2.5.x/ScalaTestingWithGuice 
    val application:Application = new GuiceApplicationBuilder() 
    .overrides(bind[DatabaseConfigProvider].to[TestDatabase.testDbProvider]) 
    .build 

    val injector:Injector = new GuiceApplicationBuilder() 
     .overrides(bind[DatabaseConfigProvider].to[TestDatabase.testDbProvider]) 
     .injector() 

    def inject[T : ClassTag]: T = injector.instanceOf[T] 

    //needed for body.consumeData 
    implicit val system = ActorSystem("storage-controller-spec") 
    implicit val materializer = ActorMaterializer() 

    protected val databaseHelper:DatabaseHelper = inject[DatabaseHelper] 

    val logger: Logger = Logger(this.getClass) 

    override def beforeAll(): Unit ={ 
    logger.warn(">>>> before all <<<<") 
    val theFuture = databaseHelper.setUpDB().map({ 
     case Success(result)=>logger.info("DB setup successful") 
     case Failure(error)=>logger.error(s"DB setup failed: $error") 
    }) 

    Await.result(theFuture, 10.seconds) 
    } 

    override def afterAll(): Unit ={ 
    logger.warn("<<<< after all >>>>") 
    Await.result(databaseHelper.teardownDB(), 10.seconds) 
    } 

    val componentName:String 
    val uriRoot:String 

    def testParsedJsonObject(checkdata:JsLookupResult,test_parsed_json:JsValue):Seq[MatchResult[Any]] 

    val testGetId:Int 
    val testGetDocument:String 
    val testCreateDocument:String 
    val testDeleteId:Int 
    val testConflictId:Int 
    val minimumNewRecordId:Int 

    def bodyAsJsonFuture(response:Future[play.api.mvc.Result]) = response.flatMap(result=> 
    result.body.consumeData.map(contentBytes=> { 
     logger.debug(contentBytes.decodeString("UTF-8")) 
     Json.parse(contentBytes.decodeString("UTF-8")) 
    } 
    ) 
) 

    componentName should { 

    "return 400 on a bad request" in { 
     logger.debug(s"$uriRoot/boum") 
     val response = route(application,FakeRequest(GET, s"$uriRoot/boum")).get 
     status(response) must equalTo(BAD_REQUEST) 
    } 

    "return valid data for a valid record" in { 
     logger.warn(s"Test URL is $uriRoot/1") 
     val response:Future[play.api.mvc.Result] = route(application, FakeRequest(GET, s"$uriRoot/1")).get 

     status(response) must equalTo(OK) 
     val jsondata = Await.result(bodyAsJsonFuture(response), 5.seconds).as[JsValue] 
     (jsondata \ "status").as[String] must equalTo("ok") 
     (jsondata \ "result" \ "id").as[Int] must equalTo(1) 
     testParsedJsonObject(jsondata \ "result", Json.parse(testGetDocument)) 
    } 

    "accept new data to create a new record" in { 
     val response = route(application, FakeRequest(
     method="PUT", 
     uri=uriRoot, 
     headers=FakeHeaders(Seq(("Content-Type", "application/json"))), 
     body=testCreateDocument) 
    ).get 

     status(response) must equalTo(OK) 
     val jsondata = Await.result(bodyAsJsonFuture(response), 5.seconds).as[JsValue] 
     (jsondata \ "status").as[String] must equalTo("ok") 
     (jsondata \ "detail").as[String] must equalTo("added") 
     (jsondata \ "id").as[Int] must greaterThanOrEqualTo(minimumNewRecordId) //if we re-run the tests without blanking the database explicitly this goes up 

     val newRecordId = (jsondata \ "id").as[Int] 
     val checkResponse = route(application, FakeRequest(GET, s"$uriRoot/$newRecordId")).get 
     val checkdata = Await.result(bodyAsJsonFuture(checkResponse), 5.seconds) 


     (checkdata \ "status").as[String] must equalTo("ok") 
     (checkdata \ "result" \ "id").as[Int] must equalTo(newRecordId) 
     testParsedJsonObject(checkdata \ "result", Json.parse(testCreateDocument)) 
    } 

    "delete a record" in { 
     val response = route(application, FakeRequest(
     method="DELETE", 
     uri=s"$uriRoot/$testDeleteId", 
     headers=FakeHeaders(), 
     body="") 
    ).get 

     status(response) must equalTo(OK) 
     val jsondata = Await.result(bodyAsJsonFuture(response), 5.seconds).as[JsValue] 
     (jsondata \ "status").as[String] must equalTo("ok") 
     (jsondata \ "detail").as[String] must equalTo("deleted") 
     (jsondata \ "id").as[Int] must equalTo(testDeleteId) 
    } 

    "return conflict (409) if attempting to delete something with sub-objects" in { 
     val response = route(application, FakeRequest(
     method = "DELETE", 
     uri = s"$uriRoot/$testConflictId", 
     headers = FakeHeaders(), 
     body = "") 
    ).get 

     status(response) must equalTo(CONFLICT) 
     val jsondata = Await.result(bodyAsJsonFuture(response), 5.seconds).as[JsValue] 
     (jsondata \ "status").as[String] must equalTo("error") 
     (jsondata \ "detail").as[String] must equalTo("This is still referenced by sub-objects") 
    } 
    } 
} 

DatabaseHelper:

class DatabaseHelper @Inject()(configuration: Configuration, dbConfigProvider: DatabaseConfigProvider) { 

    private val dbConfig = dbConfigProvider.get[JdbcProfile] 
    private val logger: Logger = Logger(this.getClass) 

    def setUpDB():Future[Try[Unit]] = { 
    logger.warn("In setUpDB") 
    dbConfig.db.run(
     DBIO.seq(
     (TableQuery[FileAssociationRow].schema ++ 
      TableQuery[FileEntryRow].schema ++ 
      TableQuery[ProjectEntryRow].schema ++ 
      TableQuery[ProjectTemplateRow].schema ++ 
      TableQuery[ProjectTypeRow].schema ++ 
      TableQuery[StorageEntryRow].schema 
     ).create, 
     TableQuery[StorageEntryRow] += StorageEntry(None,None,"filesystem",Some("me"),None,None,None), 
     TableQuery[StorageEntryRow] += StorageEntry(None,None,"omms",Some("you"),None,None,None), 
     TableQuery[FileEntryRow] += FileEntry(None,"/path/to/a/video.mxf",1,"me",1,new Timestamp(12345678),new Timestamp(12345678),new Timestamp(12345678)), 
     TableQuery[FileEntryRow] += FileEntry(None,"/path/to/secondtestfile",1,"tstuser",1,new Timestamp(123456789),new Timestamp(123456789),new Timestamp(123456789)), 
     //"""{"name": "Premiere test template 1","projectTypeId": 1,"filepath", "storageId": 1}""" 
     //"{"name":,"opensWith":"AdobePremierePro.app","targetVersion":"14.0"}" 
     TableQuery[ProjectTypeRow] += ProjectType(None,"Premiere 2014 test","AdobePremierePro.app","14.0"), 
     TableQuery[ProjectTypeRow] += ProjectType(None,"Cubase 7.0 test","Cubase.app","7.0"), 
     TableQuery[ProjectTemplateRow] += ProjectTemplate(Some(1),"Premiere test template 1",1,"/srv/projectfiles/ProjectTemplatesDev/Premiere/premiere_template_2014.prproj",1) 

    ).asTry 
    ) 
    } 

    def teardownDB():Future[Try[Unit]] = { 
    logger.warn("In teardownDB") 
    dbConfig.db.run(
     DBIO.seq(
     (
      TableQuery[FileAssociationRow].schema ++ 
      TableQuery[FileEntryRow].schema ++ 
      TableQuery[ProjectEntryRow].schema ++ 
      TableQuery[ProjectTemplateRow].schema ++ 
      TableQuery[ProjectTypeRow].schema ++ 
      TableQuery[StorageEntryRow].schema 
     ).drop 
    ).asTry 
    ) 
    } 
} 

build.sbt Einstellungen:

concurrentRestrictions in Global := Seq(
    Tags.limit(Tags.Test, 1), 
    Tags.limitAll(1) 
) 

parallelExecution in Test := false 

Antwort

0

Die Ausnahme besagt, dass Ihr Verbindungspool keine Verbindungen mehr aufweist.

Ich sehe zwei Probleme hier:

  1. Sie sind die DB-Verbindung in Ihrem Teardown nicht schließen.
  2. Sie können für viele Verbindungen pro Verbindungspool öffnen, und da jede Ihrer Spezifikationen einen neuen Verbindungspool erstellt, haben Sie kein maximales Verbindungslimit mehr auf Ihrem postgres-Server. Sie können durch die Reduzierung der „numThreads“ Parameter Ihrer slick config

Aus Performancegründen die Anzahl der Verbindungen pro Verbindungs-Pool reduzieren empfehle ich Ihnen sowieso H2DB Treiber mit Postgres Einstellungen für Unit-Tests zu verwenden, wie lange sind Sie nicht verwenden Postgres spezifische Funktionen, die H2DB nicht emulieren kann.

+0

Hallo David, Vielen Dank für Ihre Hilfe. Ich habe die Nähe zum Teardown hinzugefügt und habe Dinge lokal arbeiten lassen, hatte aber immer noch Probleme mit CI. Ich hatte begonnen, die H2DB-Treiber zu verwenden, bin aber auf Postgres umgestiegen, da ich Schwierigkeiten hatte, H2 mit Datenbank-Evolutions zu arbeiten, aber vorerst habe ich beschlossen, Entwicklungen aufzugeben und die App zum Laufen zu bringen. Das hat mir wirklich geholfen, mein Verständnis zu verbessern :) –