文档

§使用数据库进行测试

虽然可以使用 ScalaTestspecs2 编写功能测试来测试数据库访问代码,方法是启动包含数据库的完整应用程序,但启动完整应用程序通常不可取,因为需要启动和运行更多组件才能测试应用程序的一小部分。

Play 提供了许多实用程序来帮助测试数据库访问代码,这些代码允许在与应用程序的其他部分隔离的情况下使用数据库进行测试。这些实用程序可以轻松地与 ScalaTest 或 specs2 一起使用,并且可以使您的数据库测试更接近轻量级且快速运行的单元测试,而不是重量级且缓慢的功能测试。

§使用数据库

要使用数据库后端进行测试,您只需要

libraryDependencies += jdbc % Test

要连接到数据库,至少需要数据库驱动程序名称和数据库的 URL,使用 Databases 伴随对象。例如,要连接到 MySQL,您可以使用以下代码

import play.api.db.Databases

val database = Databases(
  driver = "com.mysql.jdbc.Driver",
  url = "jdbc:mysql://localhost/test"
)

这将创建一个连接到运行在localhost上的MySQL test数据库的数据库连接池,并将其命名为default。数据库名称仅在Play内部使用,例如,由其他功能(如演变)使用,以加载与该数据库关联的资源。

您可能希望为数据库指定其他配置,包括自定义名称或配置属性(如用户名、密码以及Play支持的各种连接池配置项),方法是提供自定义名称参数和/或自定义配置参数。

import play.api.db.Databases

val database = Databases(
  driver = "com.mysql.jdbc.Driver",
  url = "jdbc:mysql://localhost/test",
  name = "mydatabase",
  config = Map(
    "username" -> "test",
    "password" -> "secret"
  )
)

在使用数据库后,由于数据库通常由一个连接池支持,该连接池持有打开的连接,并且可能还具有正在运行的线程,因此您需要关闭它。这可以通过调用shutdown方法来完成。

database.shutdown()

手动创建数据库并关闭它在您使用在每个测试或套件周围运行启动/关闭代码的测试框架时很有用。否则,建议您让Play为您管理连接池。

§允许Play为您管理数据库

Play还提供了一个withDatabase帮助程序,允许您提供一个代码块以使用Play管理的数据库连接池执行。Play将确保在代码块执行完毕后正确关闭它。

import play.api.db.Databases

Databases.withDatabase(
  driver = "com.mysql.jdbc.Driver",
  url = "jdbc:mysql://localhost/test"
) { database =>
  val connection = database.getConnection()
  // ...
}

Database.apply工厂方法一样,withDatabase也允许您根据需要传递自定义nameconfig映射。

通常,直接从每个测试中使用withDatabase会产生过多的样板代码。建议您创建自己的帮助程序以删除您的测试使用的此样板。例如

import play.api.db.Database
import play.api.db.Databases

def withMyDatabase[T](block: Database => T) = {
  Databases.withDatabase(
    driver = "com.mysql.jdbc.Driver",
    url = "jdbc:mysql://localhost/test",
    name = "mydatabase",
    config = Map(
      "username" -> "test",
      "password" -> "secret"
    )
  )(block)
}

然后它可以在每个测试中轻松使用,只需最少的样板代码。

withMyDatabase { database =>
  val connection = database.getConnection()
  // ...
}

提示:您可以使用它来外部化您的测试数据库配置,使用环境变量或系统属性来配置要使用的数据库以及如何连接到它。这允许开发人员以他们喜欢的方式设置自己的环境,以及为提供可能与开发不同的特定环境的CI系统提供最大的灵活性。

§使用内存数据库

有些人更喜欢不需要安装数据库等基础设施来运行测试。Play提供简单的帮助程序来为此目的创建H2内存数据库。

import play.api.db.Databases

val database = Databases.inMemory()

内存数据库可以通过提供自定义名称、自定义URL参数和自定义连接池配置来配置。以下显示了提供MODE参数以告诉H2模拟MySQL,以及配置连接池以记录所有语句。

import play.api.db.Databases

val database = Databases.inMemory(
  name = "mydatabase",
  urlOptions = Map(
    "MODE" -> "MYSQL"
  ),
  config = Map(
    "logStatements" -> true
  )
)

与通用数据库工厂一样,请确保始终关闭内存数据库连接池。

database.shutdown()

如果您没有使用测试框架的before/after功能,您可能希望Play为您管理内存数据库的生命周期,这使用withInMemory非常简单。

import play.api.db.Databases

Databases.withInMemory() { database =>
  val connection = database.getConnection()

  // ...
}

withDatabase一样,建议您创建自己的方法来包装withInMemory调用,以减少样板代码。

import play.api.db.Database
import play.api.db.Databases

def withMyDatabase[T](block: Database => T) = {
  Databases.withInMemory(
    name = "mydatabase",
    urlOptions = Map(
      "MODE" -> "MYSQL"
    ),
    config = Map(
      "logStatements" -> true
    )
  )(block)
}

§应用演变

在运行测试时,通常需要为数据库管理数据库模式。如果您已经在使用演变,那么在测试中重复使用与开发和生产中相同的演变通常是有意义的。您可能还想创建专门用于测试的自定义演变。Play 提供了一些方便的帮助程序来应用和管理演变,而无需运行整个 Play 应用程序。

要应用演变,可以使用 Evolutions 伴生对象的 applyEvolutions 方法。

import play.api.db.evolutions._

Evolutions.applyEvolutions(database)

这将从类路径中的 evolutions/<databasename> 目录加载演变,并应用它们。

测试运行后,您可能希望将数据库重置为其原始状态。如果您以这样一种方式实现了演变的向下脚本,即它们将删除所有数据库表,那么您可以通过简单地调用 cleanupEvolutions 方法来做到这一点。

Evolutions.cleanupEvolutions(database)

§自定义演变

在某些情况下,您可能希望在测试中运行一些自定义演变。自定义演变可以使用自定义的 EvolutionsReader 来使用。其中最简单的是 SimpleEvolutionsReader,它是一个演变读取器,它接受一个预先配置的数据库名称到 Evolution 脚本序列的映射,并且可以使用 SimpleEvolutionsReader 伴生对象上的便捷方法来构造。例如

import play.api.db.evolutions._

Evolutions.applyEvolutions(
  database,
  SimpleEvolutionsReader.forDefault(
    Evolution(
      1,
      "create table test (id bigint not null, name varchar(255));",
      "drop table test;"
    )
  )
)

清理自定义演变的方式与清理常规演变相同,使用 cleanupEvolutions 方法。

Evolutions.cleanupEvolutions(database)

但请注意,您不需要在此处传递自定义演变读取器,这是因为演变的状态存储在数据库中,包括用于拆卸数据库的向下脚本。

有时将自定义演变脚本放在代码中是不切实际的。如果是这种情况,您可以使用 ClassLoaderEvolutionsReader 将它们放在测试资源目录中,位于自定义路径下。例如

import play.api.db.evolutions._

Evolutions.applyEvolutions(database, ClassLoaderEvolutionsReader.forPrefix("testdatabase/"))

这将从 testdatabase/evolutions/<databasename>/<n>.sql 加载演变,其结构和格式与开发和生产中相同。

如果您将演变脚本存储在项目文件夹之外,则可以使用 EnvironmentEvolutionsReader 从文件系统上的绝对路径或从项目文件夹中看到的相对路径加载脚本。

import play.api.Environment
import play.api.db.evolutions._

// Absolute path
Evolutions.applyEvolutions(
  database,
  new EnvironmentEvolutionsReader(Environment.simple(), "/opt/db_migration")
)

// Relative path (based on your project's root folder)
Evolutions.applyEvolutions(
  database,
  new EnvironmentEvolutionsReader(Environment.simple(), "../db_migration")
)

§允许 Play 管理演变

如果您使用测试框架来管理在测试之前和之后运行演变,则 applyEvolutionscleanupEvolutions 方法非常有用。Play 还提供了一个方便的 withEvolutions 方法来为您管理它,如果需要这种更轻量级的方案。

import play.api.db.evolutions._

Evolutions.withEvolutions(database) {
  val connection = database.getConnection()

  // ...
}

当然,withEvolutions 可以与 withDatabasewithInMemory 结合使用,以减少样板代码,使您能够定义一个既能实例化数据库又能为您运行演变的函数。

import play.api.db.Database
import play.api.db.Databases
import play.api.db.evolutions._

def withMyDatabase[T](block: Database => T) = {
  Databases.withInMemory(
    urlOptions = Map(
      "MODE" -> "MYSQL"
    ),
    config = Map(
      "logStatements" -> true
    )
  ) { database =>
    Evolutions.withEvolutions(
      database,
      SimpleEvolutionsReader.forDefault(
        Evolution(
          1,
          "create table test (id bigint not null, name varchar(255));",
          "drop table test;"
        )
      )
    ) {
      block(database)
    }
  }
}

定义了测试的自定义数据库管理方法后,我们现在就可以直接使用它们了。

withMyDatabase { database =>
  val connection = database.getConnection()
  connection.prepareStatement("insert into test values (10, 'testing')").execute()

  connection
    .prepareStatement("select * from test where id = 10")
    .executeQuery()
    .next() must_== true
}

下一步:测试 Web 服务客户端


发现此文档中的错误? 此页面的源代码可以在 此处 找到。 阅读完 文档指南 后,请随时贡献拉取请求。 有问题或建议要分享? 请访问 我们的社区论坛,与社区开始对话。