2

我试图弄清楚如何在我的 Play2 应用程序中编写我的数据库集成测试。

在我的 conf 文件中,我指定了两个数据库,xxx_test 用于常规使用,h2 db 用于测试:

db.xxx_test.driver=com.mysql.jdbc.Driver
db.xxx_test.url="jdbc:mysql://localhost/xxx_test?characterEncoding=UTF-8"
db.xxx_test.user="root"
db.xxx_test.password=""

db.h2.driver=org.h2.Driver
db.h2.url="jdbc:h2:mem:play"
db.h2.user=sa
db.h2.password=""

在我的用户对象中,我指定xxx_test在运行应用程序时使用。

def createUser(user: User): Option[User] = {

    DB.withConnection("xxx_test") {
      implicit connection =>
        SQL("insert into users(first_name, last_name, email, email_validated, last_login, created, modified, active) values({first_name},{last_name},{email},{email_validated},{last_login}, {created}, {modified}, {active})").on(
          'first_name -> user.firstName,
          'last_name -> user.lastName,
          'email -> user.email,
          'email_validated -> user.emailValidated,
          'last_login -> user.lastLogin,
          'created -> user.created,
          'modified -> user.modified,
          'active -> true
        ).executeInsert().map(id => {
          return Some(User(new Id[Long](id), user.firstName, user.lastName, user.email, user.emailValidated, user.lastLogin, user.created, user.modified, true))
        }
        )
    }
    None
  }

在我的测试中,我创建了一个新的 inMemoryDatabase,然后使用 User 创建并获取我的对象进行测试。

class DBEvolutionsTest extends Specification {

  "The Database" should {
    "persist data properly" in {
      running(FakeApplication(additionalConfiguration = inMemoryDatabase())) {

        User.create(User(Id[Long](1L), "jakob",
          "aa",
          "aaa",
          true,
          DateTime.now(),
          DateTime.now(),
          DateTime.now(),
          true)) 

        val newUser = User.findBy(Id[Long](1L))

        newUser.get.firstName must beEqualTo("jakob")
      }
      }
    }

}

这当然不是正确的方法,因为 User 对象使用的是xxx_test而不是 h2db。测试将在真实数据库中创建一个用户,而不是在内存中创建一个用户,因为我在 User( DB.withConnection("xxx_test")) 对象中指定了数据库。我想有一些聪明的方法可以做到这一点,我不想在应用程序中传递数据库名称,比如User.create(User(...), "xxx_test")

你是如何解决这个问题的?

4

3 回答 3

3

您可能想查看如何在 scala 中进行依赖注入。一个好的解决方案是将数据库从用户模型中抽象出来,然后将其作为依赖项传递。

一种简单的方法是更改​​配置文件以进行测试。Play 让您指定在命令行上使用哪个配置文件。虽然这不是最实用的。

另一种解决方案是使用隐式,将数据库连接定义为函数的隐式参数:

def createUser(user: User)(implicit dbName: String): Option[User]= 
  DB.withConnection(dbName) { ... }

您仍然必须在所有调用中向上传播参数,但您可以隐藏它: def importUsers(csvFile: File)(implicit dbName: String): Seq[User] = { conn => ... User.createUser( u) ... }

当你从顶部调用它时:

implicit dbName = "test"
importUsers(...)

这是内置在 scala 中的,所以它很容易设置并且不需要很多样板来支持它。就个人而言,我认为隐式使代码不清楚,我更喜欢本演示文稿中提出的解决方案Dead-Simple Dependency Injection

它的要点是,您使您createUser和所有其他依赖于数据库连接的方法根据连接返回一个函数,而不仅仅是结果。以下是它如何与您的示例一起使用。

1-您创建一个配置连接的连接特征。一个简单的形式是:

trait ConnectionConfig {
   def dbName: String
}

2-您的方法取决于该配置返回一个函数:

def createUser(user: User): ConnectionConfig => Option[User] = { conn =>

    DB.withConnection(conn.dbName) { ... }

}

3-当您在另一个方法中使用 createUser 时,该方法也变得依赖于连接,因此您可以通过返回对 ConnectionConfig 的依赖关系以及函数返回类型来标记它,例如:

def importUsers(csvFile: File): ConnectionConfig => Seq[User] = { conn =>
    ...
    User.createUser(u)(conn)
    ...
}

这是一个好习惯,因为在您的代码中会清楚哪些方法取决于与数据库的连接,并且您可以轻松地交换连接。因此,在您的主应用程序中,您将创建真正的连接:

class RealConncetionConfig extends ConnectionConfig {
   val dbName = "xxx_test"
}

但是在您的测试文件中,您创建了一个测试数据库配置:

class DBEvolutionsTest extends Specification {

  class TestDBConfig extends ConnectionConfig {
      val dbName = "h2"
  }

  val testDB = new TestDBConfig()

  "The Database" should {
    "persist data properly" in {
      running(FakeApplication(additionalConfiguration = inMemoryDatabase())) {

        User.create(User(Id[Long](1L), "jakob",
          "aa",
          "aaa",
          true,
          DateTime.now(),
          DateTime.now(),
          DateTime.now(),
          true))(testDB) 

        val newUser = User.findBy(Id[Long](1L))

        newUser.get.firstName must beEqualTo("jakob")
      }
      }
    }

}

这是它的要点。查看我提到的演示文稿和幻灯片,有一种很好的方法可以将所有这些抽象化,这样您就可以摆脱(conn)使这段代码变得丑陋的论点。

作为旁注,如果我是你,我什至会抽象 DB 的类型。因此,与其将 SQL 放在 User 模型对象中,不如将其放在单独的实现中,这样您就可以轻松切换数据库的类型(使用 mongodb、dynamo...)。它就像这样,从前面的代码扩展而来:

trait ConnectionConfig {
   def createUser(user: User): Option[User]
}

在用户模型对象中:

def createUser(user: User): ConnectionConfig => Option[User] = { conn =>
    conn.createUser(user)
}

这样,在根据用户模型测试部分代码时,您可以制作一个模拟数据库,其中 createUser 始终有效并返回预期结果(或始终失败......),甚至无需使用内存数据库(您仍然会需要测试真正的 SQL 连接,但您可以测试应用程序的其他部分):

trait ConnectionConfig {
    def createUser(user: User): Option[User] = Some(user)
}
于 2013-03-20T14:30:25.920 回答
0

您必须为内存数据库定义一个不同于默认 (xxx_test) 名称的名称。我认为以下代码段应该有效。

FakeApplication(additionalConfiguration = inMemoryDatabase("h2"))

另请参阅:https ://stackoverflow.com/a/11029324/2153190

于 2013-03-20T10:13:43.933 回答
0

inMemoryDatabase方法定义如下:

def inMemoryDatabase(
  name: String = "default", 
  options: Map[String, String] = Map.empty[String, String]): Map[String, String]

我的猜测是您应该将 thexxx_test作为name参数传递。

于 2013-03-19T23:33:52.553 回答