在 Play2/Specs2 中进行 Slick/Postgres 数据库测试时遇到问题
Trouble with Slick/Postgres database testing in Play2/Specs2
我在构建应用程序测试时遇到了很多问题,希望有更多 Scala 经验的人能为我指明正确的方向。
我有许多驻留在 Postgres 数据库中的数据模型,使用 Slick 映射到案例 类。然后,My Play 应用程序向所述数据模型提供基于 JSON 的 REST 端点。
由于大多数实际代码在每个端点之间都是相似的,因此大多数代码被实现为混合到实际控制器中的特征,它覆盖了必要的位。
这工作正常,但是当我尝试 运行 对它们中的每一个进行单元测试时,大多数控制器都工作,然后我得到了错误:
[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.apply(Classes.scala:104)
[error] org.specs2.control.ActionT$$anonfun$safe.apply(ActionT.scala:88)
[error] org.specs2.control.ActionT$$anonfun$reader$$anonfun$apply.apply(ActionT.scala:79)
[error] org.specs2.control.Status$.safe(Status.scala:100)
[error] org.specs2.control.StatusT$$anonfun$safe.apply(StatusT.scala:62)
[error] org.specs2.control.StatusT$$anonfun$safe.apply(StatusT.scala:62)
[error] scalaz.syntax.ToApplicativeOps$$anon.self$lzycompute(ApplicativeSyntax.scala:29)
[error] scalaz.syntax.ToApplicativeOps$$anon.self(ApplicativeSyntax.scala:29)
[error] scalaz.syntax.ToApplicativeOps$ApplicativeIdV$$anonfun$point.apply(ApplicativeSyntax.scala:33)
[error] scalaz.WriterTApplicative$$anonfun$point.apply(WriterT.scala:282)
[error] scalaz.WriterTApplicative$$anonfun$point.apply(WriterT.scala:282)
[error] scalaz.effect.IO$$anonfun$apply$$anonfun$apply.apply(IO.scala:136)
[error] scalaz.effect.IO$$anonfun$apply$$anonfun$apply.apply(IO.scala:136)
[error] scalaz.FreeFunctions$$anonfun$return_.apply(Free.scala:326)
[error] scalaz.FreeFunctions$$anonfun$return_.apply(Free.scala:326)
[error] scalaz.std.FunctionInstances$$anon$$anonfun$map.apply(Function.scala:56)
[error] scalaz.Free$$anonfun$run.apply(Free.scala:172)
[error] scalaz.Free$$anonfun$run.apply(Free.scala:172)
[error] scalaz.Free.go2(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.unsafePerformIO(IO.scala:227)
[error] org.specs2.reflect.Classes$$anonfun$createInstance$$anonfun$apply$$anonfun.apply(Classes.scala:37)
[error] org.specs2.reflect.Classes$$anonfun$createInstance$$anonfun$apply$$anonfun.apply(Classes.scala:36)
[error] scala.collection.immutable.List.map(List.scala:273)
[error] org.specs2.reflect.Classes$$anonfun$createInstance$$anonfun$apply.apply(Classes.scala:36)
[error] org.specs2.reflect.Classes$$anonfun$createInstance$$anonfun$apply.apply(Classes.scala:29)
[error] scala.Function1$$anonfun$andThen.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.apply(StatusT.scala:16)
[error] org.specs2.control.StatusT$$anonfun$map.apply(StatusT.scala:16)
[error] scalaz.WriterT$$anonfun$map.apply(WriterT.scala:46)
[error] scalaz.WriterT$$anonfun$map.apply(WriterT.scala:46)
[error] scalaz.effect.IO$$anonfun$map$$anonfun$apply.apply(IO.scala:56)
[error] scalaz.effect.IO$$anonfun$map$$anonfun$apply.apply(IO.scala:55)
[error] scalaz.Free$$anonfun$map.apply(Free.scala:52)
[error] scalaz.Free$$anonfun$map.apply(Free.scala:52)
[error] scalaz.Free$$anonfun$flatMap$$anonfun$apply.apply(Free.scala:60)
[error] scalaz.Free$$anonfun$flatMap$$anonfun$apply.apply(Free.scala:60)
[error] scalaz.Free.resume(Free.scala:72)
[error] scalaz.Free.go2(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.unsafePerformIO(IO.scala:227)
[error] org.specs2.runner.SbtRunner$$anonfun$newTask$$anon.execute(SbtRunner.scala:37)
[error] sbt.ForkMain$Run.call(ForkMain.java:294)
[error] sbt.ForkMain$Run.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 是一个设置我的测试数据的对象。
减少规范的数量会使错误消失,所以我知道问题不在于那个特定的测试规范。
我正在按照 Play 文档的建议使用 运行time DI 并覆盖数据库绑定进行测试,以避免清空我的开发数据库。
我正在使用 Play evolutions 来管理数据库架构,但在 setup/teardown 期间故意清空数据库并重新设置它,以确保它在每个测试规范之前都是原始的。
我认为发生的事情是我所有的测试规范都在同时初始化,这意味着它们都试图同时连接到数据库,因此 运行没连接了。
我曾尝试使用 sbt 中的 parallelExecution 和 concurrentRestrictions 设置一次只设置一个进程,但这无济于事。我也试过按顺序将每个规格设置为 运行 但这似乎不起作用。我也尝试捕获异常并重试设置,但这似乎也不起作用。
我现在不知道该怎么做才能让我的测试成功!请帮忙。
非常感谢。
测试用例:
@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")
}
}
}
数据库助手:
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 设置:
concurrentRestrictions in Global := Seq(
Tags.limit(Tags.Test, 1),
Tags.limitAll(1)
)
parallelExecution in Test := false
异常告诉您您的连接池没有连接。
我在这里看到两个问题:
- 您没有在拆解中关闭数据库连接。
- 您可以为每个连接池打开许多连接,并且由于您的每个规范都在创建一个新的连接池,因此您 运行 超出了 postgres 服务器上配置的最大连接数。您可以通过减少 slick config
的 "numThreads" 参数来减少每个连接池的连接数
由于性能原因,我建议您无论如何使用带有 postgres 设置的 H2DB 驱动程序进行单元测试,只要您不使用 H2DB 无法模拟的 postgres 特定功能。
我在构建应用程序测试时遇到了很多问题,希望有更多 Scala 经验的人能为我指明正确的方向。
我有许多驻留在 Postgres 数据库中的数据模型,使用 Slick 映射到案例 类。然后,My Play 应用程序向所述数据模型提供基于 JSON 的 REST 端点。 由于大多数实际代码在每个端点之间都是相似的,因此大多数代码被实现为混合到实际控制器中的特征,它覆盖了必要的位。
这工作正常,但是当我尝试 运行 对它们中的每一个进行单元测试时,大多数控制器都工作,然后我得到了错误:
[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.apply(Classes.scala:104)
[error] org.specs2.control.ActionT$$anonfun$safe.apply(ActionT.scala:88)
[error] org.specs2.control.ActionT$$anonfun$reader$$anonfun$apply.apply(ActionT.scala:79)
[error] org.specs2.control.Status$.safe(Status.scala:100)
[error] org.specs2.control.StatusT$$anonfun$safe.apply(StatusT.scala:62)
[error] org.specs2.control.StatusT$$anonfun$safe.apply(StatusT.scala:62)
[error] scalaz.syntax.ToApplicativeOps$$anon.self$lzycompute(ApplicativeSyntax.scala:29)
[error] scalaz.syntax.ToApplicativeOps$$anon.self(ApplicativeSyntax.scala:29)
[error] scalaz.syntax.ToApplicativeOps$ApplicativeIdV$$anonfun$point.apply(ApplicativeSyntax.scala:33)
[error] scalaz.WriterTApplicative$$anonfun$point.apply(WriterT.scala:282)
[error] scalaz.WriterTApplicative$$anonfun$point.apply(WriterT.scala:282)
[error] scalaz.effect.IO$$anonfun$apply$$anonfun$apply.apply(IO.scala:136)
[error] scalaz.effect.IO$$anonfun$apply$$anonfun$apply.apply(IO.scala:136)
[error] scalaz.FreeFunctions$$anonfun$return_.apply(Free.scala:326)
[error] scalaz.FreeFunctions$$anonfun$return_.apply(Free.scala:326)
[error] scalaz.std.FunctionInstances$$anon$$anonfun$map.apply(Function.scala:56)
[error] scalaz.Free$$anonfun$run.apply(Free.scala:172)
[error] scalaz.Free$$anonfun$run.apply(Free.scala:172)
[error] scalaz.Free.go2(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.unsafePerformIO(IO.scala:227)
[error] org.specs2.reflect.Classes$$anonfun$createInstance$$anonfun$apply$$anonfun.apply(Classes.scala:37)
[error] org.specs2.reflect.Classes$$anonfun$createInstance$$anonfun$apply$$anonfun.apply(Classes.scala:36)
[error] scala.collection.immutable.List.map(List.scala:273)
[error] org.specs2.reflect.Classes$$anonfun$createInstance$$anonfun$apply.apply(Classes.scala:36)
[error] org.specs2.reflect.Classes$$anonfun$createInstance$$anonfun$apply.apply(Classes.scala:29)
[error] scala.Function1$$anonfun$andThen.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.apply(StatusT.scala:16)
[error] org.specs2.control.StatusT$$anonfun$map.apply(StatusT.scala:16)
[error] scalaz.WriterT$$anonfun$map.apply(WriterT.scala:46)
[error] scalaz.WriterT$$anonfun$map.apply(WriterT.scala:46)
[error] scalaz.effect.IO$$anonfun$map$$anonfun$apply.apply(IO.scala:56)
[error] scalaz.effect.IO$$anonfun$map$$anonfun$apply.apply(IO.scala:55)
[error] scalaz.Free$$anonfun$map.apply(Free.scala:52)
[error] scalaz.Free$$anonfun$map.apply(Free.scala:52)
[error] scalaz.Free$$anonfun$flatMap$$anonfun$apply.apply(Free.scala:60)
[error] scalaz.Free$$anonfun$flatMap$$anonfun$apply.apply(Free.scala:60)
[error] scalaz.Free.resume(Free.scala:72)
[error] scalaz.Free.go2(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.unsafePerformIO(IO.scala:227)
[error] org.specs2.runner.SbtRunner$$anonfun$newTask$$anon.execute(SbtRunner.scala:37)
[error] sbt.ForkMain$Run.call(ForkMain.java:294)
[error] sbt.ForkMain$Run.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 是一个设置我的测试数据的对象。
减少规范的数量会使错误消失,所以我知道问题不在于那个特定的测试规范。
我正在按照 Play 文档的建议使用 运行time DI 并覆盖数据库绑定进行测试,以避免清空我的开发数据库。
我正在使用 Play evolutions 来管理数据库架构,但在 setup/teardown 期间故意清空数据库并重新设置它,以确保它在每个测试规范之前都是原始的。
我认为发生的事情是我所有的测试规范都在同时初始化,这意味着它们都试图同时连接到数据库,因此 运行没连接了。
我曾尝试使用 sbt 中的 parallelExecution 和 concurrentRestrictions 设置一次只设置一个进程,但这无济于事。我也试过按顺序将每个规格设置为 运行 但这似乎不起作用。我也尝试捕获异常并重试设置,但这似乎也不起作用。
我现在不知道该怎么做才能让我的测试成功!请帮忙。
非常感谢。
测试用例:
@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")
}
}
}
数据库助手:
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 设置:
concurrentRestrictions in Global := Seq(
Tags.limit(Tags.Test, 1),
Tags.limitAll(1)
)
parallelExecution in Test := false
异常告诉您您的连接池没有连接。
我在这里看到两个问题:
- 您没有在拆解中关闭数据库连接。
- 您可以为每个连接池打开许多连接,并且由于您的每个规范都在创建一个新的连接池,因此您 运行 超出了 postgres 服务器上配置的最大连接数。您可以通过减少 slick config 的 "numThreads" 参数来减少每个连接池的连接数
由于性能原因,我建议您无论如何使用带有 postgres 设置的 H2DB 驱动程序进行单元测试,只要您不使用 H2DB 无法模拟的 postgres 特定功能。