在play 2.0 scala中的同一个FakeApplication()中运行多个测试

时间:2012-08-19 16:47:10

标签: unit-testing scala playframework-2.0 specs2

我正在尝试学习Play scala中的单元测试,但我遇到了一些问题。我试图在我的模型层上运行几个测试,如下所示:

"User Model" should {
    "be created and retrieved by username" in {
        running(FakeApplication()) {
            val newUser = User(username = "weezybizzle",password = "password")
            User.save(newUser)
            User.findOneByUsername("weezybizzle") must beSome
        }
    }
    "another test" in {
        running(FakeApplication()) {
            // more tests involving adding and removing users
        }
    }
}

然而,当这样做时,我无法在第二次单元测试中连接到数据库,说连接已关闭。我试图通过将所有代码包含在同一个假应用程序上运行的块中来解决这个问题,但这也无法解决。

  running(FakeApplication()) {
    "be created and retrieved by username" in {
        val newUser = User(username = "weezybizzle",password = "password")
        User.save(newUser)
        User.findOneByUsername("weezybizzle") must beSome
    }
    "another test" in {
        // more tests involving adding and removing users
    }
  }

7 个答案:

答案 0 :(得分:14)

默认情况下,specs2测试是并行执行的,这可能会导致访问数据库时出现问题,尤其是当您依赖先前测试提供的db内容时。因此,要强制进行顺序测试,您必须告诉specs2:

class ModelSpec extends Specification with Logging {
  override def is = args(sequential = true) ^ super.is
...
}

对于在一个FakeApplication中完成的测试,您可以将整个测试包装在其中:

  running(FakeApp) {
    log.trace("Project tests.")
    val Some(project) = Project.findByName("test1")

    "Project" should {

      "be retrieved by name" in {
        project must beAnInstanceOf[Project]
        project.description must endWith("project")
      }

可以找到整个样本here。这是我用Play测试MongoDB时第一次尝试处理问题!框架。

我从salat项目中借用的第二种方法,这是处理MongoDB的一个非常好的规范示例来源(尽管它不是Play!框架应用程序)。您必须定义一个扩展AroundScope的特征,您可以在其中放置需要在应用程序实例中初始化的任何内容:

import org.specs2.mutable._
import org.specs2.execute.StandardResults

import play.api.mvc._
import play.api.mvc.Results
import play.api.test._
import play.api.test.Helpers._

trait FakeApp extends Around with org.specs2.specification.Scope {

  val appCfg = Map(
    "first.config.key" -> "a_value",
    "second.config.key" -> "another value"
  )

  object FakeApp extends FakeApplication(
      additionalPlugins = Seq("com.github.rajish.deadrope.DeadropePlugin"),
      additionalConfiguration = appCfg
    ) {
    // override val routes = Some(Routes)
  }

  def around[T <% org.specs2.execute.Result](test: => T) = running(FakeApp) {
    Logger.debug("Running test ==================================")
    test  // run tests inside a fake application
  }
}

修改2013-06-30:

在当前版本的specs2中,around签名应为:

def around[T : AsResult](test: => T): Result

编辑结束

然后可以这样写一个测试:

class SomeSpec extends Specification { sequential // according to @Eric comment

  "A test group" should {
    "pass some tests" in new FakeApp {
      1 must_== 1
    }

    "and these sub-tests too" in {
      "first subtest" in new FakeApp {
         success
      }
      "second subtest" in new FakeApp {
         failure
      }
    }
  }
}

可以找到此类套件的完整示例here

最后说明:在启动套件之前清理测试数据库也很好:

  step {
    MongoConnection().dropDatabase("test_db")
  }

答案 1 :(得分:3)

在进行集成测试/运行测试套件时,我们遇到了诸如“CacheManager已关闭。它已无法再使用”或“SQLException:尝试从已关闭的池中获取连接”的预期。他们都与每次测试后重新启动应用程序有关。 我们最终做了一个相当简单的特性,它将在每次测试之前检查正在运行的FakeApplication,并且只在需要时启动一个。

trait SingleInstance extends BeforeExample {
    def before() {
        if (Play.unsafeApplication == null) Play.start(AppWithTestDb)
    }
}

object AppWithTestDb extends FakeApplication(additionalConfiguration = 
    Map("db.default.url" -> "jdbc:mysql://localhost/test_db")
)

然后在测试中:

class SampleSpec extends PlaySpecification with SingleInstance {
    "do something" should {
        "result in something" in {
        }
    }
}

这适用于Play 2.3以及Play 2.4

答案 2 :(得分:1)

一种更清洁的方法

import play.api.test._

trait ServerSpec {

  implicit val app: FakeApplication = FakeApplication()
  implicit def port: Port = Helpers.testServerPort

  val server = TestServer(port, app)
}

然后将其与

一起使用
class UsersSpec extends PlaySpecification with Results with ServerSpec {

  "Users Controller" should {

    step(server.start())

    "get users" in {
      val result = Users.query().apply(FakeRequest())

      val json = contentAsJson(result)
      val stat = status(result)

      stat mustEqual 200
    }

    step(server.stop())
  }
}

答案 3 :(得分:0)

为了针对数据库测试您的代码,如果您使用提供的内存来测试它,您应该在running调用中告诉它:

FakeApplication(additionalConfiguration = inMemoryDatabase())

以某种方式,这将强制你的数据库开始和停止内部块执行(无论是单个还是组合)

修改

由于评论说您正在使用mongodb,我建议您阅读此blog,其中我正在谈论我编写的一个小插件,以使mongodb服务器能够启动嵌入式

我们要做的是(通过启用插件)在应用程序的同时启动和停止mongodb。

它可以帮助你......

然而关于最初的问题,问题不应该来自运行或FakeApplication,除非Play-Salat或任何其他相关插件正在进行错误的连接或缓存或...

答案 4 :(得分:0)

在许多情况下使用运行方法时会发生这种并行测试问题。但这已在play2.1中修复。 Here是如何修复的。如果你想在play2.0.x中运行这个,你应该像这样做:

trait TestUtil {
  /**
   * Executes a block of code in a running application.
   */
  def running[T](fakeApp: FakeApplication)(block: => T): T = {
     synchronized {
      try {
        Play.start(fakeApp)
        block
      } finally {
        Play.stop()
        play.core.Invoker.system.shutdown()
        play.core.Invoker.uninit()
      }
    }
  }

  /**
   * Executes a block of code in a running server.
   */
  def running[T](testServer: TestServer)(block: => T): T = {
    synchronized {
      try {
        testServer.start()
        block
      } finally {
        testServer.stop()
        play.core.Invoker.system.shutdown()
        play.core.Invoker.uninit()
      }
    }
  }
}

您可以使用以下内容:

class ModelSpec extends Specification with TestUtil {
    "User Model" should {
        "be created and retrieved by username" in {
            running(FakeApplication()) {
                val newUser = User(username = "weezybizzle",password = "password")
                User.save(newUser)
                User.findOneByUsername("weezybizzle") must beSome
            }
        }
    }
    ....

答案 5 :(得分:0)

我发现Scala运行单个测试类FakeApplication的最佳方法是遵循 以下示例。注意'step'方法:

@RunWith(classOf[JUnitRunner])
class ContaControllerSpec extends MockServices {

    object contaController extends ContaController with MockAtividadeService with MockAccountService with MockPessoaService with MockTelefoneService with MockEmailService{
        pessoaService.update(PessoaFake.id.get, PessoaFake) returns PessoaFake.id.get
    }

    step(Play.start(new FakeAppContext))

    "ContaController [Perfil]" should {

      "atualizar os dados do usuario logado e retornar status '200' (OK)" in {
          val response = contaController.savePerfil()(FakeRequest(POST, "/contas/perfil").withFormUrlEncodedBody(
              ("nome", "nome teste"), ("sobrenome", "sobrenome teste"), ("dataNascimento", "1986-09-12"), ("sexo", "M")).withLoggedIn(config)(uuid))

              status(response) must be equalTo(OK)
        }

        "atualizar os dados do usuario logado enviando o form sem preenchimento e retornar status '400' (BAD_REQUEST)" in {
            val response = contaController.savePerfil()(FakeRequest(POST, "/contas/perfil").withLoggedIn(config)(uuid))
            status(response) must be equalTo(BAD_REQUEST)
        }
    }

    step(Play.stop)
}

答案 6 :(得分:0)

接受的答案对我没有帮助。我在玩2.2.3 scala 2.10.3。这对我有所帮助。

可能会有所帮助。

扩展BoneCPPlugin

class NewBoneCPPlugin(val app: play.api.Application) extends BoneCPPlugin(app) {


  override def onStop() {
    //don't stop the BoneCPPlugin
    //plugin.onStop()
  }
}

在你的testspec中应该是

    class UserControllerSpec extends mutable.Specification with Logging with Mockito {

    val fakeApp = FakeApplication(additionalConfiguration = testDb,withoutPlugins = Seq("play.api.db.BoneCPPlugin"),
                                  additionalPlugins = Seq("NewBoneCPPlugin"))
    "Create action in UserController " should {
            "return 400 status if request body does not contain user json " in new WithApplication(fakeApp) {
        ...
    }
  }
}