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
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 :) –