Writing functional tests with ScalaTest

用ScalaTest写功能测试

为你的应用编写测试可是一个复杂的过程。Play提供了帮助手册和应用存根。并且ScalaTest提供了一个整合库,ScalaTest + Play,使测试你的应用变得尽可能简单。

总览

测试文件位于“test”目录下。

你可以通过Play控制台运行测试。

  1. 点击test运行所有测试。
  2. 点击类名后跟随的test-only,比如,test-only my.namespace.MySpec 来运行一个测试类。
  3. 点击test-quick运行只有已经失败的测试。
  4. 输入一个前面带~的命令,比如 ~test-quick来持续性的运行测试。
  5. 运行test:console来在控制台中查看测试帮助手册如 FakeApplication.

在Play中测试是基于SBT,testing SBT章节中提供了更加详细的信息。

使用 ScalaTest + Play

为了能够使用ScalaTest + Play, 你将需要将它添加到你的构建中,通过改变projects/Build.scala 例如:

  1. val appDependencies = Seq(
  2. // 在此处添加你的项目依赖,
  3. "org.scalatestplus" %% "play" % "1.1.0" % "test"
  4. )

你不需要显性的添加ScalaTest到你的构建中。适当版本的ScalaTest会作为ScalaTest + Play的一个过渡的依赖关系。你将需要选择一个和你的Play版本匹配的一个版本的ScalaTest + Play,你可以通过ScalaTest + Play的versions页面。

ScalaTest + Play中,你可以通过扩展PlaySpec的特性来定义测试类。

例如这个例子:

  1. import collection.mutable.Stack
  2. import org.scalatestplus.play._
  3. class StackSpec extends PlaySpec {
  4. "A Stack" must {
  5. "pop values in last-in-first-out order" in {
  6. val stack = new Stack[Int]
  7. stack.push(1)
  8. stack.push(2)
  9. stack.pop() mustBe 2
  10. stack.pop() mustBe 1
  11. }
  12. "throw NoSuchElementException if an empty stack is popped" in {
  13. val emptyStack = new Stack[Int]
  14. a [NoSuchElementException] must be thrownBy {
  15. emptyStack.pop()
  16. }
  17. }
  18. }
  19. }

你或者可以通过定义自己的基类来替换使用PlaySpec.

你可以与Play一起或在IntelliJ IDEA(使用Scala plugin)或Eclipse(使用Scala IDEScalaTest Eclipse plugin)中运行你的测试。请通过IDE页面来获取更多详细信息。

适配器

PlaySpec混合了ScalaTest的MustMatchers,所以你可以通过使用ScalaTest的适配器DSL来写声明变量:

  1. Hello world must endWith ("world")

请参考MustMatchers的文档来获取更多信息。

Mockito

你可以通过使用mocks来隔离单元测试需要的外部依赖。例如,如果你的类依赖于一个外部类DataService,你可以为你的类采集适当的数据而不需要实例化一个DataService对象。

ScalaTest通过MockitoSuger特性提供与Mockito集成。

为了使用Mockito,混合MockitoSUger到你的测试类中然后使用Mockito库来模拟依赖关系:

  1. case class Data(retrievalDate: java.util.Date)
  2. trait DataService {
  3. def findData: Data
  4. }
  5. import org.scalatest._
  6. import org.scalatest.mock.MockitoSugar
  7. import org.scalatestplus.play._
  8. import org.mockito.Mockito._
  9. class ExampleMockitoSpec extends PlaySpec with MockitoSugar {
  10. "MyService#isDailyData" should {
  11. "return true if the data is from today" in {
  12. val mockDataService = mock[DataService]
  13. when(mockDataService.findData) thenReturn Data(new java.util.Date())
  14. val myService = new MyService() {
  15. override def dataService = mockDataService
  16. }
  17. val actual = myService.isDailyData
  18. actual mustBe true
  19. }
  20. }
  21. }

模拟对于测试类的公共方法是非常有用的。模拟对象和私有方法是可能的,但是难以实现。

单元测试模型

Play不需要通过模式去使用一个特定的数据库数据访问层。而且,如果应用使用Anorm或Slick,该模式往往将拥有一个数据库访问的内部引用。

  1. import anorm._
  2. import anorm.SqlParser._
  3. case class User(id: String, name: String, email: String) {
  4. def roles = DB.withConnection { implicit connection =>
  5. ...
  6. }
  7. }

对于单元测试,这种方法可以微妙的模拟roles方法。

一个通用的方法是保持模式尽可能的逻辑上从数据库分离,并抽象数据库的访问于一个仓库层的后面。

  1. case class Role(name:String)
  2. case class User(id: String, name: String, email:String)
  3. trait UserRepository {
  4. def roles(user:User) : Set[Role]
  5. }
  6. class AnormUserRepository extends UserRepository {
  7. import anorm._
  8. import anorm.SqlParser._
  9. def roles(user:User) : Set[Role] = {
  10. ...
  11. }
  12. }

然后通过服务访问它们:

  1. class UserService(userRepository : UserRepository) {
  2. def isAdmin(user:User) : Boolean = {
  3. userRepository.roles(user).contains(Role("ADMIN"))
  4. }
  5. }

通过这种方式, isAdmin方法可以通过模拟出UserRepository引用并传递其到该服务中来测试:

  1. class UserServiceSpec extends PlaySpec with MockitoSugar {
  2. "UserService#isAdmin" should {
  3. "be true when the role is admin" in {
  4. val userRepository = mock[UserRepository]
  5. when(userRepository.roles(any[User])) thenReturn Set(Role("ADMIN"))
  6. val userService = new UserService(userRepository)
  7. val actual = userService.isAdmin(User("11", "Steve", "user@example.org"))
  8. actual mustBe true
  9. }
  10. }
  11. }

单元测试控制器

在Play中控制器被定义为对象,所以更难来进行单元测试。在Play中可以通过依赖注入使用getControllerInstance来缓解。另一种方式去巧妙处理包含一个控制器的单元测试是针对这个控制器使用一个隐式类型自引用的一种特质:

  1. trait ExampleController {
  2. this: Controller =>
  3. def index() = Action {
  4. Ok("ok")
  5. }
  6. }
  7. object ExampleController extends Controller with ExampleController

并接着测试这个特质:

  1. import scala.concurrent.Future
  2. import org.scalatest._
  3. import org.scalatestplus.play._
  4. import play.api.mvc._
  5. import play.api.test._
  6. import play.api.test.Helpers._
  7. class ExampleControllerSpec extends PlaySpec with Results {
  8. class TestController() extends Controller with ExampleController
  9. "Example Page#index" should {
  10. "should be valid" in {
  11. val controller = new TestController()
  12. val result: Future[SimpleResult] = controller.index().apply(FakeRequest())
  13. val bodyText: String = contentAsString(result)
  14. bodyText mustBe "ok"
  15. }
  16. }
  17. }

单元测试基本动作

测试Action或Filter需要测试一个EssentialAction(关于什么是EssentialAction的详细信息)

对此,测试Helpers.call可以像这样使用:

  1. class ExampleEssentialActionSpec extends PlaySpec {
  2. "An essential action" should {
  3. "can parse a JSON body" in {
  4. val action: EssentialAction = Action { request =>
  5. val value = (request.body.asJson.get \ "field").as[String]
  6. Ok(value)
  7. }
  8. val request = FakeRequest(POST, "/").withJsonBody(Json.parse("""{ "field": "value" }"""))
  9. val result = call(action, request)
  10. status(result) mustEqual OK
  11. contentAsString(result) mustEqual "value"
  12. }
  13. }
  14. }