文档

§使用 specs2 测试您的应用程序

为您的应用程序编写测试可能是一个复杂的过程。Play 为您提供了一个默认的测试框架,并提供帮助程序和应用程序存根,使测试您的应用程序尽可能容易。

§概述

测试的位置在“test”文件夹中。在 test 文件夹中创建了两个示例测试文件,可以用作模板。

您可以从 Play 控制台运行测试。

Play 中的测试基于 sbt,完整的描述可在 testing sbt 章节中找到。

§使用 specs2

要使用 Play 的 specs2 支持,请将 Play specs2 依赖项添加至您的构建中,作为测试范围内的依赖项。

libraryDependencies += specs2 % Test

specs2 中,测试被组织成规范,规范包含示例,这些示例通过各种不同的代码路径运行被测系统。

规范扩展了 Specification 特性,并使用 should/in 格式。

import org.specs2.mutable._

class HelloWorldSpec extends Specification {
  "The 'Hello world' string" should {
    "contain 11 characters" in {
      "Hello world" must have size 11
    }
    "start with 'Hello'" in {
      "Hello world" must startWith("Hello")
    }
    "end with 'world'" in {
      "Hello world" must endWith("world")
    }
  }
}

规范可以在 IntelliJ IDEA(使用 Scala 插件)或 Eclipse(使用 Scala IDE)中运行。有关更多详细信息,请参阅 IDE 页面

注意:由于 演示编译器 中的错误,测试必须以特定格式定义才能与 Eclipse 协同工作。

以下是一个适用于 Eclipse 的有效规范。

package models

import org.junit.runner.RunWith
import org.specs2.mutable.Specification
import org.specs2.runner.JUnitRunner

@RunWith(classOf[JUnitRunner])
class UserSpec extends Specification {
  "User" should {
    "have a name" in {
      val user = User(id = "user-id", name = "Player", email = "[email protected]")
      user.name must beEqualTo("Player")
    }
  }
}

§匹配器

当您使用示例时,您必须返回一个示例结果。通常,您会看到一个包含 must 的语句。

"Hello world" must endWith("world")

must 关键字之后的表达式称为 匹配器。匹配器返回一个示例结果,通常是成功或失败。如果示例没有返回结果,则无法编译。

最有用的匹配器是 匹配结果。这些用于检查相等性,确定 Option 和 Either 的结果,甚至检查是否抛出异常。

还有一些 可选匹配器,允许在测试中进行 XML 和 JSON 匹配。

§Mockito

模拟用于隔离针对外部依赖项的单元测试。例如,如果您的类依赖于外部 DataService 类,您可以向您的类提供适当的数据,而无需实例化 DataService 对象。

要使用 Mockito(一个流行的模拟库),请添加以下导入。

import org.mockito.Mockito._

您可以像这样模拟对类的引用。

trait DataService {
  def findData: Data
}

case class Data(retrievalDate: java.util.Date)
import java.util._

import org.mockito.Mockito._
import org.specs2.mutable._

class ExampleMockitoSpec extends Specification {
  "MyService#isDailyData" should {
    "return true if the data is from today" in {
      val mockDataService = mock(classOf[DataService])
      when(mockDataService.findData).thenReturn(Data(retrievalDate = new java.util.Date()))

      val myService = new MyService() {
        override def dataService = mockDataService
      }

      val actual = myService.isDailyData
      actual must equalTo(true)
    }
  }
}

模拟测试对于测试类的公共方法特别有用。模拟对象和私有方法是可能的,但要困难得多。

§单元测试模型

Play 不要求模型使用特定的数据库数据访问层。但是,如果应用程序使用 Anorm 或 Slick,那么模型通常会在内部引用数据库访问。

import anorm._
import anorm.SqlParser._

case class User(id: String, name: String, email: String) {
   def roles = DB.withConnection { implicit connection =>
      ...
    }
}

对于单元测试,这种方法可能会使模拟 roles 方法变得很棘手。

一种常见的方法是将模型与数据库和尽可能多的逻辑隔离,并将数据库访问抽象到一个存储库层。

case class Role(name: String)

case class User(id: String, name: String, email: String)
trait UserRepository {
  def roles(user: User): Set[Role]
}
class AnormUserRepository extends UserRepository {
  import anorm._
  import anorm.SqlParser._

  def roles(user:User) : Set[Role] = {
    ...
  }
}

然后通过服务访问它们

class UserService(userRepository: UserRepository) {

  def isAdmin(user: User): Boolean = {
    userRepository.roles(user).contains(Role("ADMIN"))
  }
}

这样,isAdmin 方法可以通过模拟 UserRepository 引用并将其传递给服务来进行测试

class UserServiceSpec extends Specification {
  "UserService#isAdmin" should {
    "be true when the role is admin" in {
      val userRepository = mock(classOf[UserRepository])
      when(userRepository.roles(any[User])).thenReturn(Set(Role("ADMIN")))

      val userService = new UserService(userRepository)
      val actual      = userService.isAdmin(User("11", "Steve", "[email protected]"))
      actual must beTrue
    }
  }
}

§单元测试控制器

由于您的控制器只是普通的类,因此您可以使用 Play 助手轻松地对其进行单元测试。如果您的控制器依赖于其他类,使用 依赖注入 将使您能够模拟这些依赖项。例如,给定以下控制器

class ExampleController @Inject() (cc: ControllerComponents) extends AbstractController(cc) {
  def index = Action {
    Ok("ok")
  }
}

您可以像这样测试它

import javax.inject.Inject

import scala.concurrent.Future

import play.api.data.FormBinding.Implicits._
import play.api.i18n.Messages
import play.api.mvc._
import play.api.test._

class ExampleControllerSpec extends PlaySpecification with Results {
  "Example Page#index" should {
    "be valid" in {
      val controller             = new ExampleController(Helpers.stubControllerComponents())
      val result: Future[Result] = controller.index.apply(FakeRequest())
      val bodyText: String       = contentAsString(result)
      (bodyText must be).equalTo("ok")
    }
  }
}

§StubControllerComponents

StubControllerComponentsFactory 创建一个模拟的 ControllerComponents,可用于对控制器进行单元测试

val controller = new MyController(
  Helpers.stubControllerComponents(bodyParser = stubParser)
)

§StubBodyParser

StubBodyParserFactory 创建一个模拟的 BodyParser,可用于对内容进行单元测试

val stubParser = Helpers.stubBodyParser(AnyContent("hello"))

§单元测试表单

表单也只是一般的类,可以使用 Play 的测试助手进行单元测试。使用 FakeRequest,您可以调用 form.bindFromRequest 并针对任何自定义约束测试错误。

为了对表单处理和渲染验证错误进行单元测试,你需要在隐式作用域中有一个 MessagesApi 实例。 MessagesApi 的默认实现是 DefaultMessagesApi

您可以像这样测试它

object FormData {
  import play.api.data._
  import play.api.data.Forms._
  import play.api.i18n._
  import play.api.libs.json._

  val form = Form(
    mapping(
      "name" -> text,
      "age"  -> number(min = 0)
    )(UserData.apply)(UserData.unapply)
  )

  case class UserData(name: String, age: Int)
  object UserData {
    def unapply(u: UserData): Option[(String, Int)] = Some(u.name, u.age)
  }
}

class ExampleFormSpec extends PlaySpecification with Results {
  import play.api.data._
  import play.api.i18n._
  import play.api.libs.json._
  import FormData._

  "Form" should {
    "be valid" in {
      val messagesApi = new DefaultMessagesApi(
        Map(
          "en" ->
            Map("error.min" -> "minimum!")
        )
      )
      implicit val request: FakeRequest[AnyContentAsFormUrlEncoded] = {
        FakeRequest("POST", "/")
          .withFormUrlEncodedBody("name" -> "Play", "age" -> "-1")
      }
      implicit val messages: Messages = messagesApi.preferred(request)

      def errorFunc(badForm: Form[UserData]) = {
        BadRequest(badForm.errorsAsJson)
      }

      def successFunc(userData: UserData) = {
        Redirect("/").flashing("success" -> "success form!")
      }

      val result = Future.successful(form.bindFromRequest().fold(errorFunc, successFunc))
      Json.parse(contentAsString(result)) must beEqualTo(Json.obj("age" -> Json.arr("minimum!")))
    }
  }
}

当渲染一个使用表单帮助器的模板时,你可以像传递其他参数一样传递 Messages,或者使用 Helpers.stubMessages()

class ExampleTemplateSpec extends PlaySpecification {
  import play.api.data._
  import FormData._

  "Example Template with Form" should {
    "be valid" in {
      val form: Form[UserData]        = FormData.form
      implicit val messages: Messages = Helpers.stubMessages()
      contentAsString(views.html.formTemplate(form)) must contain("ok")
    }
  }
}

或者,如果你使用的是一个使用 CSRF.formField 并且需要隐式请求的表单,你可以在模板中使用 MessagesRequest,并使用 Helpers.stubMessagesRequest()

class ExampleTemplateWithCSRFSpec extends PlaySpecification {
  import play.api.data._
  import FormData._

  "Example Template with Form" should {
    "be valid" in {
      val form: Form[UserData]                                 = FormData.form
      implicit val messageRequestHeader: MessagesRequestHeader = Helpers.stubMessagesRequest()
      contentAsString(views.html.formTemplateWithCSRF(form)) must contain("ok")
    }
  }
}

§单元测试 EssentialAction

测试 ActionFilter 可能需要测试一个 EssentialAction (有关 EssentialAction 的更多信息)

为此,测试 Helpers.call() 可以这样使用

class ExampleEssentialActionSpec extends PlaySpecification {
  "An essential action" should {
    "can parse a JSON body" in new WithApplication() with Injecting {
      override def running() = {
        val Action = inject[DefaultActionBuilder]
        val parse  = inject[PlayBodyParsers]

        val action: EssentialAction = Action(parse.json) { request =>
          val value = (request.body \ "field").as[String]
          Ok(value)
        }

        val request = FakeRequest(POST, "/").withJsonBody(Json.parse("""{ "field": "value" }"""))

        val result = call(action, request)

        status(result) mustEqual OK
        contentAsString(result) mustEqual "value"
      }
    }
  }
}

§单元测试 Messages

为了进行单元测试,DefaultMessagesApi 可以不带参数实例化,它将接受一个原始映射,因此你可以针对自定义的 MessagesApi 测试表单和验证失败。

class ExampleMessagesSpec extends PlaySpecification with ControllerHelpers {
  import play.api.data.Form
  import play.api.data.FormBinding.Implicits._
  import play.api.data.Forms._
  import play.api.i18n._
  import play.api.libs.json.Json

  case class UserData(name: String, age: Int)
  object UserData {
    def unapply(u: UserData): Option[(String, Int)] = Some(u.name, u.age)
  }

  "Messages test" should {
    "test messages validation in forms" in {
      // Define a custom message against the number validation constraint
      val messagesApi = new DefaultMessagesApi(
        Map("en" -> Map("error.min" -> "CUSTOM MESSAGE"))
      )

      // Called when form validation fails
      def errorFunc(badForm: Form[UserData])(implicit request: RequestHeader) = {
        implicit val messages: Messages = messagesApi.preferred(request)
        BadRequest(badForm.errorsAsJson)
      }

      // Called when form validation succeeds
      def successFunc(userData: UserData) = Redirect("/")

      // Define an age with 0 as the minimum
      val form = Form(
        mapping("name" -> text, "age" -> number(min = 0))(UserData.apply)(UserData.unapply)
      )

      // Submit a request with age = -1
      implicit val request: FakeRequest[AnyContentAsFormUrlEncoded] = {
        play.api.test
          .FakeRequest("POST", "/")
          .withFormUrlEncodedBody("name" -> "Play", "age" -> "-1")
      }

      // Verify that the "error.min" is the custom message
      val result = Future.successful(form.bindFromRequest().fold(errorFunc, successFunc))
      Json.parse(contentAsString(result)) must beEqualTo(Json.obj("age" -> Json.arr("CUSTOM MESSAGE")))
    }
  }
}

你也可以在测试中使用 Helpers.stubMessagesApi() 来提供一个预先准备好的空 MessagesApi

下一步:使用 specs2 编写功能测试


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