15

我正在尝试学习 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
    }
  }
4

7 回答 7

15

The specs2 tests are performed by default in parallel which may cause problems with accessing databases, especially when you rely on the db contents provided by a previous test. So to force sequential testing you have to tell specs2 to do so:

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

For tests done in one FakeApplication you can wrap the whole tests in it:

  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")
      }

The whole sample can be found here. That was my first attempt to deal with problems while testing MongoDB with Play! framework.

The second approach I borrowed from the salat project, which is by the way a very good source of specs examples dealing with MongoDB (although it is not a Play! framework app). You have to define a trait extending Around and Scope, where you can put anything you need to be initialized in an application instance:

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
  }
}

Edit 2013-06-30:

In the current version of specs2 the around signature should be:

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

End of edit

Then a test can be written like that:

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
      }
    }
  }
}

A full sample of such suite can be found here.

On a final note: It's also good to clean up the test database before starting a suite:

  step {
    MongoConnection().dropDatabase("test_db")
  }
于 2012-08-20T22:23:26.250 回答
3

While doing integration testing/running test suites, we ran into expections like "The CacheManager has been shut down. It can no longer be used" or "SQLException: Attempting to obtain a connection from a pool that has already been shutdown". They were all related to restart the app after each test. We finally made a rather simple trait, that will check for a running FakeApplication before each test, and start one only, if required.

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")
)

And then in the test:

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

This will work for Play 2.3 as well as Play 2.4

于 2015-09-08T11:47:23.710 回答
1

A somewhat cleaner approach

import play.api.test._

trait ServerSpec {

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

  val server = TestServer(port, app)
}

And then use it with

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())
  }
}
于 2014-01-16T09:03:44.627 回答
0

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

FakeApplication(additionalConfiguration = inMemoryDatabase())

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

编辑

Due to the comment saying that you're using a mongodb, I'd recommend you to read this blog wherein I'm talking about a small plugin I wrote to enable a mongodb server to start embedded-like.

What we'll be done is (by enabling the plugin) to start and stop a mongodb at the same time of the application.

It could help you...

However regarding the initial question the problem shouldn't come from the running or the FakeApplication, unless Play-Salat or any other related plugin is doing bad connections or caching or ...

于 2012-08-19T20:06:38.847 回答
0

This kind of parallel testing problem happen when using running method in many case. But this is already fixed in play2.1. Here is how to fix. If you want to use this running in play2.0.x, you should make trait like this:

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()
      }
    }
  }
}

And you can use the following:

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
            }
        }
    }
    ....
于 2013-01-06T08:02:28.313 回答
0

The best way I found to run a single test class FakeApplication by Scala, is following the example below. Note the'step' method:

@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)
}
于 2013-12-20T16:49:38.320 回答
0

The accepted answer did not help me. I am on play 2.2.3 scala 2.10.3. This is what helped me.

May be it could be of some help.

Extend BoneCPPlugin

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


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

And in your testspec should be

    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) {
        ...
    }
  }
}
于 2014-07-02T16:13:11.643 回答