在Play 2.0 Scala中,在同一个FakeApplication()中运行多个测试。

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
    }
  }
7个回答

15

默认情况下,specs2测试是并行执行的,这可能会导致访问数据库时出现问题,特别是当您依赖于先前测试提供的数据库内容时。因此,要强制进行顺序测试,您需要告诉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")
      }

整个示例可以在此处找到。这是我第一次尝试使用Play!框架测试MongoDB时遇到的问题。
我借鉴了salat项目的第二种方法,它是处理MongoDB的specs示例的一个非常好的来源(尽管它不是一个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
      }
    }
  }
}

这样的套件完整示例可以在这里找到。

最后注意:在开始测试套件之前清理测试数据库也是很好的做法:

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

请注意,您可以使用以下代码替换原有代码:"class SomeSpec extends Specification {sequential}",而不是写成:"class SomeSpec extends Specification {override def is = args(sequential = true) ^ super.is}"。 - Eric
@Eric 谢谢!我在文档中没有找到这个。这是最近的功能吗? - Rajish
实际上,我似乎仍然有问题,在这里发布了堆栈跟踪 https://dev59.com/TWfWa4cB1Zd3GeqPks8_ - wfbarksdale

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


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

非常有帮助!我甚至使用了更简单的“running(TestServer(3333, FakeApplication()))”。 - ozma

0

当在许多情况下使用 running 方法时,会出现这种并行测试问题。但是已经在 play2.1 中修复了此问题。这里是如何解决的。如果您想在 play2.0.x 中使用此运行方法,则应该制作类似于以下的 trait:

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

0

我没有从已接受的答案中获得帮助。我使用的是play 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()
  }
}

而在你的测试规范中应该是这样的

    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) {
        ...
    }
  }
}

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

0
为了针对数据库测试您的代码,在使用提供的内存数据库进行测试时,您应该在running调用中告诉它:
FakeApplication(additionalConfiguration = inMemoryDatabase())

以某种方式,这将强制您的数据库在内部块执行期间启动和停止(无论是单个还是组合)。
编辑
由于评论中提到您正在使用mongodb,我建议您阅读此blog,其中我谈到了我编写的一个小插件,以使mongodb服务器能够像嵌入式一样启动。
我们将要做的事情是(通过启用插件)同时启动和停止应用程序的mongodb。
它可能会对您有所帮助...
但是关于最初的问题,问题不应该来自运行或FakeApplication,除非Play-Salat或任何其他相关插件正在进行错误的连接、缓存或...

很遗憾,我正在使用mongodb,所以我不认为我可以使用内存数据库。 - wfbarksdale
我之前自己编写了一个Mongo插件,并且有一个专门针对测试的子类,只有在第一个测试开始时才连接到数据库,并且在该测试结束时不会断开连接。虽然不是最理想的解决方案,但它完成了工作。 - Alex Varju
要使用specs2实现这样的目标,您只需要在启动Mongo的Fragments开头使用Step.One,在结束时使用另一个Step。您应该在定义“is”方法的trait中使用它们来围绕您的测试使用它们。 - Andy Petrella
请在此页面中查找“Template”,该页面准确地说明了要做什么:http://etorreborre.github.com/specs2/guide/org.specs2.guide.Structure.html - Andy Petrella

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接