§使用 specs2 测试您的应用程序
为您的应用程序编写测试可能是一个复杂的过程。Play 为您提供了一个默认的测试框架,并提供帮助程序和应用程序存根,使测试您的应用程序尽可能容易。
§概述
测试的位置在“test”文件夹中。在 test 文件夹中创建了两个示例测试文件,可以用作模板。
您可以从 Play 控制台运行测试。
- 要运行所有测试,请运行
test
。 - 要仅运行一个测试类,请运行
test-only
,后跟类的名称,例如test-only my.namespace.MySpec
。 - 要仅运行失败的测试,请运行
test-quick
。 - 要持续运行测试,请运行以波浪号开头的命令,例如
~test-quick
。 - 要访问测试帮助程序(如
FakeRequest
)在控制台中,请运行Test/console
。
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 协同工作。
- 包必须与目录路径完全相同。
- 规范必须使用
@RunWith(classOf[JUnitRunner])
进行注释。
以下是一个适用于 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
测试 Action
或 Filter
可能需要测试一个 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 编写功能测试
发现文档中的错误? 此页面的源代码可以在 这里 找到。 阅读完 文档指南 后,请随时贡献拉取请求。 有问题或建议要分享? 请访问 我们的社区论坛 与社区进行交流。