能干Grails – 利用Grails举办单位测试(单位测试提速)
当前位置:以往代写 > JAVA 教程 >能干Grails – 利用Grails举办单位测试(单位测试提速)
2019-06-14

能干Grails – 利用Grails举办单位测试(单位测试提速)

能干Grails – 利用Grails举办单位测试(单位测试提速)

副标题#e#

在本期能干Grails中,Scott Davis 向您展示如何操作 Grails 中包括的 GrailsUnitTestCase 和 ControllerUnitTestCase 类的内置模仿成果。

Grails 支持两种根基的测试范例:单位测试和集成测试。两种测试的语法完 全沟通:都被利用沟通的断言编写为一个 GroovyTestCase。它们之间的区别在 于语义上。单位测试用于在断绝情况下测试类,而集成测试支持在完整的、正在 运行的情况中测试类。

该文章是按照其时最新的 Grails 1.0 版本编写的,在该版本中,测试基本 架构的成果获得了显著改造。GrailsUnitTestCase 类及其子类的引入将流程测 试的简朴性和全面性晋升到了一个全新的程度。详细来讲,这些新测试类的模仿 成果晋升了单位测试的速度,同时可以或许像在集成测试中一样正常测试成果。图 1 展示了 Grails 1.1.x 中全新的测试条理布局:

图 1. Grails 1.1.x 中全新的测试条理布局

醒目Grails - 操作Grails举行单元测试(单元测试提速)

当您在下一节中建设一个新的域类和节制器时,您将相识如何实际应用 GrailsUnitTestCase 和 ControllerUnitTestCase。

开始

要执行本文中的示例,首先建设一个新应用措施。在呼吁提示符下键入:

grails create-app testing

变动到测试目次(cd testing),然后键入:

grails create-domain-class User

接下来键入:

grails create-controller User

将清单 1 中的代码添加到 grails-app/domain/User.groovy 中:

清单 1. User 域类

class User {
  String name
  String login
  String password
  String role = "user"

  static constraints = {
   name(blank:false)
   login(unique:true, blank:false)
   password(password:true, minSize:5)
   role(inList:["user", "admin"])
  }

  String toString(){
   "${name} (${role})"
  }
}

界说 grails-app/controller/UserController.groovy 的焦点行为,如清单 2 所示:

清单 2. UserController 类

class UserController {
   def scaffold = true
}

此刻根基的基本架构已经停当了,接下来添加一些测试。


#p#副标题#e#

在 GrailsUnitTestCase 中举办模仿

在文本编辑器中打开 test/unit/UserTests.groovy。代码如清单 3 所示:

清单 3. UserTests 类

import grails.test.*

class UserTests extends GrailsUnitTestCase {
   protected void setUp() {
     super.setUp()
   }

   protected void tearDown() {
     super.tearDown()
   }

   void testSomething() {

   }
}

在 Grails 1.0 中,create-domain-class 呼吁建设的存根测试扩展了 GroovyTestCase。可以看到,此刻对一个域类的单位测试(在 Grails 1.1 中) 扩展了 GrailsUnitTestCase。所以,您可以利用一些新要领来在单位测试中启 用模仿成果,这种成果在以前需要在集成测试中启用。

详细来讲,GrailsUnitTestCase 提供了以下模仿要领:

mockForConstraintsTests()

mockDomain()

mockLogging()

要领略这些模仿要领有何用途,首先建设一个会失败的测试。将 testSomething() 要领变动为 testBlank() 要领,如清单 4 所示:

清单 4. 一个将会失败的测试

void testBlank() {
  def user = new User()
  assertFalse user.validate()
}

您大概会问这个测试为什么会失败,究竟它的语法是正确的。谜底是您此刻 运行的是单位测试。单位测试意味着在断绝情况中运行,所以不会运行数据库和 Web 处事器,最重要的是不会产生与 Grails 相关的元编程。

转头看一下 清单 1 中 User 域类的源代码,很明明个中没有界说任何 validate() 要领。此要领(以及 save()、list()、hasErrors() 和您熟悉的所 有其他 Groovy Object Relational Mapping (GORM) 要领)城市被 Grails 在 运行时动态添加到域类中。

要运行这个将会失败的测试,在呼吁提示符处键入 grails test-app。您应 该看到清单 5 所示的功效:

清单 5. 节制台输出中显示的失败测试

$ grails test-app
Environment set to test

Starting unit tests ...
Running tests of type 'unit'
-------------------------------------------------------
Running 2 unit tests...
Running test UserControllerTests...PASSED
Running test UserTests...
           testBlank...FAILED
Tests Completed in 1434ms ...
-------------------------------------------------------
Tests passed: 1
Tests failed: 1
-------------------------------------------------------

Starting integration tests ...
Running tests of type 'integration'
No tests found in test/integration to execute ...

Tests FAILED - view reports in  /testing/test/reports.

#p#副标题#e#

#p#分页标题#e#

在查察失败陈诉之前,您是否留意到单位测试运行速度很快,而在运行集成 测试时会有明明的延迟?键入 grails test-app -unit 运行单位测试。纵然测 试仍然失败了,您也应该会看到测试运行速度上的显著改造。

虽然,您可以键入 grails test-app -integration 来仅运行集成测试。事 实上,您甚至可以将具有单位和集成符号与测试类的名称组合在一起。键入 grails test-app -unit User 定位到您感乐趣的特定测试类。(留意,您在名 称后头省略了 Tests 后缀,能键入更少的内容始终是一件功德)。在现实世界 中,将测试限制到单个类的本领可以或许使您对编写测试布满信心。

知道您拥有一个失败的测试之后,您大概但愿查察错误动静。在 Web 欣赏器 中打开 test/reports/html/index.html。单击失败的测试类。将会看到如图 2 所示的功效:

图 2. 陈诉显示了失败的单位测试

醒目Grails - 操作Grails举行单元测试(单元测试提速)

No signature of method: User.validate() 错误动静证实,Grails 确实没 有将 validate() 要领元编程到 User 类上。

此刻,您拥有两个选择。第一个选择是将此测试类转移到集成目次中。可是 Grails 转向运行集成测试需要很长时间,所以此选择不太抱负。第二个选择是 模仿验证行为并将测试类保存在单位目次中。

领略 mockForConstraintsTests()

要在单位测试中模仿 Grails 验证,添加 mockForConstraintsTests() 要领 ,如清单 6 所示。此要领指示 Grails 将验证要领元编程到指定的域类上,就 像凡是在运行时所做的一样。

清单 6. 将会通过的测试,这得益于 mockForConstraintsTests()

void testBlank() {
  mockForConstraintsTests(User)
  def user = new User()
  assertFalse user.validate()
}

此刻,运行测试来验证它是否会通过,如清单 7 所示:

清单 7. 运行将会通过的测试

$ grails test-app -unit  User
Environment set to test

Starting unit tests ...
Running tests of type 'unit'
-------------------------------------------------------
Running 1 unit test...
Running test UserTests...PASSED
Tests Completed in 635ms ...
-------------------------------------------------------
Tests passed: 1
Tests failed: 0
-------------------------------------------------------

Tests PASSED - view reports in  /testing/test/reports.

#p#副标题#e#

要进一步细化单位测试,您可以断言验证会因为特定字段上的特定约束而失 败,如清单 8 所示。mockForConstraintsTests() 要领将 errors 荟萃元编程 到域类上。此 errors 荟萃简化了对是否触发了正确的约束的验证。

清单 8. 断言特定字段上的一个特定约束违规

void testBlank()  {
  mockForConstraintsTests(User)
  def user = new User()
  assertFalse user.validate()

  println "=" * 20
  println "Total number of errors:"
  println user.errors.errorCount

  println "=" * 20
  println "Here are all of the errors:"
  println user.errors

  println "=" * 20
  println "Here are the errors individually:"
  user.errors.allErrors.each{
   println it
   println "-" * 20
  }

  assertEquals "blank", user.errors["name"]
}

从头运行此测试。它还会心外地失败吗?查察陈诉输入(如图 3 所示),找 出问题来源:

图 3. 用空值替代空格导致的失败

醒目Grails - 操作Grails举行单元测试(单元测试提速)

错误动静为 expected:<[blank]> but was:<[nullable]>。验 证失败了,但原因并不是您所期望的那样。

很容易碰着这种错误。在 Grails 中,默认环境下,域类中的所有字段必需 非空。这项隐含限制的问题在于,您凡是会通过 HTML 表单与 Grails 交互。如 果在 HTML 表单中将 String 字段保存为空,paramsMap 中的节制器会将其看作 空 String(也就是 ""),而不是 null。

#p#分页标题#e#

假如单击 HTML 陈诉底部的 System.out 链接,可以看到 3 个 String 字段 (name、login 和 password)都抛出了 nullable 约束违规错误。图 4 显示了 println 挪用的输出。只有 role 字段 — 其默认值为 user — 通过隐含的 nullable 约束。

图 4. 测试的 System.out 输出

醒目Grails - 操作Grails举行单元测试(单元测试提速)

#p#副标题#e#

再次调解 testBlank() 测试,确保验证因符合的原因而失败(从而使单位测 试通过),如清单 9 所示:

清单 9. 测试此刻因正确的原因得以通过

void testBlank()  {
  mockForConstraintsTests(User)
  def user = new User(name:"",
            login:"admin",
            password:"wordpass")
  assertFalse user.validate()
  assertEquals 1, user.errors.errorCount
  assertEquals "blank", user.errors["name"]
}

在从头运行测试以确保其通过期,可以办理一个稍微棘手一些的约束: unique。

利用 mockForConstraintsTests() 测试 unique 约束

在上一节已经看到,可以在断绝情况中轻松执行对他大都约束的测试。譬喻 ,测试 password 字段的 minSize 至少为 5 很是简朴,因为它只依赖于字段本 身的值。清单 10 给出了 testPassword() 要领:

清单 10. 测试 minSize 约束

void testPassword() {
  mockForConstraintsTests(User)
  def user = new User(password:"foo")
  assertFalse user.validate()
  assertEquals "minSize", user.errors["password"]
}

可是如何测试 unique 这样的约束呢?这种约束确保数据库表不包括反复值 。幸运的是,mockForConstraintsTests() 还接管第二个参数:一个用于模仿真 实数据库表的域类列表(替代真实的数据库表)。清单 11 演示了利用模仿表测 试 unique 约束的进程:

清单 11. 利用模仿表测试 unique 约束

void testUniqueLogin (){
  def jdoe = new User(name:"John Doe",
            login:"jdoe",
            password:"password")

  def suziq = new User(name:"Suzi Q",
             login:"suziq",
             password:"wordpass")

  mockForConstraintsTests(User, [jdoe, suziq])

  def jane = new User(login:"jdoe")
  assertFalse jane.validate()
  assertEquals "unique", jane.errors["login"]
}

在内存中模仿数据库表可以节减大量时间,尤其是在启动实际数据库需要很 长时间时。更糟的是,一旦数据库开始运行,您仍然需要确保利用使您的断言得 以通过所必须的记录来填凑数据库表。

我并不是体现运行对出产数据库运行实际的集成测试时挥霍时间。我的意思 是,这些耗时的集成测试更适合于一连集成处事器。在这种环境下,模仿数据库 交互可以使您专注于 Grails 成果,只花少部门时间来举办测试。

模仿数据库表已超出了 mockForConstraintsTests() 要领的本领范畴。您可 以利用 mockDomain() 要领完成这件事。

领略 mockDomain()

GORM 将一些有用的要领元编程到域类上: save()、list() 和很多定位措施 ,好比 findAllByRole()。顾名思义,mockForConstraintsTests() 要领将验证 要领添加到域类上,以举办测试。mockDomain() 要领将耐久性要领添加到域类 上,以举办测试。清单 12 展示了 mockDomain() 要领的实际应用:

清单 12. 利用 mockDomain() 测试 GORM 要领

void  testMockDomain(){
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")
  def jsmith = new User(name:"Jane Smith", role:"user")

  mockDomain(User, [jdoe, suziq, jsmith])

  //dynamic finder
  def list = User.findAllByRole("admin")
  assertEquals 1, list.size()

  //NOTE: criteria, Hibernate Query Language (HQL)
  //   and Query By Example (QBE) are not supported
}

#p#副标题#e#

mockDomain() 要领尽大概忠实地建模 GORM 行为。譬喻,当您将一个域类保 存到模仿表在中时,会像在实际应用措施中一样填充 id 字段。id 值只是列表 中元素的序数值。清单 13 展示了在单位测试中生存 域类:

清单 13. 将一个域类生存到单位测试中

#p#分页标题#e#

void testMockGorm() {
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")
  def jsmith = new User(name:"Jane Smith", role:"user")

  mockDomain(User, [jdoe, suziq, jsmith])

  def foo = new User(login:"foo")
  foo.name = "Bubba"
  foo.role = "user"
  foo.password = "password"
  foo.save()
  assertEquals 4, foo.id //NOTE: id gets assigned
  assertEquals 3, User.findAllByRole("user").size()
}

模仿底层数据库并不是您独一可以在 GrailsUnitTestCase 中完成的事情。 您也可以模仿日志基本架构。

领略

mockLogging()

GrailsUnitTestCase

的用途并不只仅是测试域类。键入 grails create-service Admin 建设一个 Admin 处事,如清单 14 所示:

醒目Grails - 操作Grails举行单元测试(单元测试提速)

模仿和元编程的范围性

mockDomain() 要领只是简朴地操作底层 Groovy 语言的本灵活态成果。(要 相识 Groovy 中的元编程的更多信息,请查阅 “实战 Groovy:利用闭包、 ExpandoMetaClass 和种别举办元编程。”)实际上,它会拦截凡是存在于域类 上的要领挪用,并将它们替换为模仿行为,以举办测试。毫无疑问,这意味着不 会模仿其他支持技能,好比条件块、 Hibernate Query Language (HQL) 和 Query By Example (QBE)。假如您的代码依赖于这些技能中的任何一种,您需要 编写集成测试并运行一个实际的数据库。

清单 14. 建设处事

$ grails create-service Admin

Created Service for Admin
Created Tests for Admin

毫无疑问,AdminService.groovy 文件会呈此刻 grails-app/services 目次 中。假如查察 test/unit 目次,应该会看到一个名为 AdminServiceTests.groovy 的 GrailsUnitTestCase。

向 AdminService 添加一个假设性要领,仅答允 admin 脚色中的用户重启服 务器,如清单 15 所示:

清单 15. 将 restart() 要领添加到 AdminService

class  AdminService {
  boolean transactional = true

  def restartServer(User user) {
   if(user.role == "admin"){
    //restart the server
    return true
   }else{
    log.info "Ha! ${user.name} thinks s/he is an  admin..."
    return false
   }
  }
}

#p#副标题#e#

对此处事的测试很是简朴。将 testRestartServer() 要领添加到 test/unit/AdminServiceTests.groovy,如清单 16 所示:

清单 16. 一个将会失败的处事测试

void testRestartServer()  {
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")

  //NOTE: no DI in unit tests
  def adminService = new AdminService()
  assertTrue adminService.restartServer(suziq)
  assertFalse adminService.restartServer(jdoe)
}

当在呼吁提示符处键入 grails test-app -unit AdminService 来运行此测 试时,将会失败。就像最初的 User 测试运行一样,导致它失败的原因并不是您 所期望的那样。看一下 HTML 陈诉,会发明熟悉的 No such property: log for class: AdminService 动静,如图 5 所示:

图 5. 依赖性注入的缺乏导致了单位测试失败

醒目Grails - 操作Grails举行单元测试(单元测试提速)

可是,这次失败并不是因为域类上缺少元编程,而是因为缺少依赖性注入。 详细来讲,所有 Grails 工件城市在运行时被注入一个 log 工具,以便它们可 以轻松地记录动静,以供将来审核。

要注入一个模仿日志记录措施以供测试,将 AdminService 类封装到一个 mockLogging() 要领挪用中,如清单 17 所示:

清单 17. 此测试将通过,这得益于 mockLogging()

void  testRestartServer() {
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")

  mockLogging(AdminService)
  def adminService = new AdminService()
  assertTrue adminService.restartServer(suziq)
  assertFalse adminService.restartServer(jdoe)
}

这一次,与预期的一样,测试通过了。所有日志输出都被发送到 System.out 。请记着,您可以在 HTML 陈诉中看到此输出。

领略 ControllerUnitTestCase

#p#分页标题#e#

利用 GrailsUnitTestCase,可以轻松测试域类和处事,但测试节制器还需要 其他一些成果。ControllerUnitTestCase 扩展了 GrailsUnitTestCase,所以您 仍然可以像以前一样利用 mockForConstraintsTests()、mockDomain() 和 mockLogging()。并且 ControllerUnitTestCase 为您正在测试的节制器建设一 个新实例,并将其存储在名为 controller 的变量中。这个 controller 变量可 用于在测试期间以编程方法与节制器交互。

要更好地领略焦点节制器的成果,在呼吁提示符处键入 grails generate- controller User。这将 def scaffold = true 替换为节制器代码的完全实现。

在完全实现的 grails-app/controllers/UserController.groovy 文件中, 您可以看到,挪用 index 操纵会重定向到 list 操纵,如清单 18 所示:

清单 18. UserController 中默认的 index 操纵

class  UserController {

   def index = { redirect(action:list,params:params) }

}

要验证是否按预期产生了重定向,将一个 testIndex() 要领添加到 test/unit/UserControllerTests.groovy,如清单 19 所示:

清单 19. 测试默认的 index 操纵

import  grails.test.*

class UserControllerTests extends ControllerUnitTestCase {
   void testIndex() {
    controller.index()
    assertEquals controller.list, controller.redirectArgs ["action"]
   }
}

#p#副标题#e#

可以看到,您首先挪用节制器操纵,就像它是另一个节制器上的要领一样。 redirect 参数存储一个名为 redirectArgs 的 Map 中。断言验证 action 键是 否包括 list 值。(假如操纵以一个 render 竣事,那么您可以按照名为 renderArgs 的 Map 举办断言)。

此刻假设 index 操纵稍微先进一些。它查抄一个 User 的会话并按照用户是 否为 admin 来重定向会话。在 ControllerUnitTestCase 中,session 和 flash 都是 Map,您可以在挪用或挪用之后的断言之前对它们举办填充。变动 index 操纵,如清单 20 所示:

清单 20. 越发先进的 index 操纵

def index = {
  if(session?.user?.role == "admin"){
   redirect(action:list,params:params)
  }else{
   flash.message = "Sorry, you are not authorized to view  this list."
   redirect(controller:"home", action:index)
  }
}

要测试这项新成果,变动 UserControllerTests.groovy 中的 testIndex() 要领,如清单 21 所示:

清单 21. 测试 session 和 flash 值

void testIndex() {
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")

  controller.session.user = jdoe
  controller.index()
  assertEquals "home", controller.redirectArgs["controller"]
  assertTrue controller.flash.message.startsWith("Sorry")

  controller.session.user = suziq 
  controller.index()
  assertEquals controller.list, controller.redirectArgs ["action"]
}

一些节制器操纵需要传入参数。在 ControllerUnitTestCase 中,您可以将 值添加到 params Map 中,就像将值添加到 flash 和 session 一样。清单 22 给出了默认的 show 操纵:

清单 22. 默认的 show 操纵

def show = {
   def userInstance = User.get( params.id )

   if(!userInstance) {
     flash.message = "User not found with id  ${params.id}"
     redirect(action:list)
   }
   else { return [ userInstance : userInstance ] }
}

还记得 GrailsUnitTestCase 的 mockDomain() 要领吗?您可以在这里利用 它来模仿 User 表,如清单 23 所示:

清单 23. 测试默认的 show 操纵

void testShow() {
  def jdoe = new User(name:"John Doe",
            login:"jdoe",
            password:"password",
            role:"user")

  def suziq = new User(name:"Suzi Q",
            login:"suziq",
            password:"wordpass",
            role:"admin")

  mockDomain(User, [jdoe, suziq])

  controller.params.id = 2 

  // this is the HashMap returned by the show action
  def returnMap = controller.show()
  assertEquals "Suzi Q", returnMap.userInstance.name
}

#p#副标题#e#

利用 ControllerUnitTestCase 测试 RESTful Web 处事

#p#分页标题#e#

有时,要测试节制器,您需要会见原始的请求和响应。对付 ControllerUnitTestCase,您可以别离通过 controller.request 和 controller.response 工具获取以下信息: GrailsMockHttpServletRequest 和 GrailsMockHttpServletResponse。

您可以查阅 “能干 Grails:RESTful Grails” 获取配置 RESTful 处事的 指南。再团结 “实战 Groovy:构建息争析 XML” 阐明功效,您就具备了测试 RESTful Web 处事所需的一切了。

将一个简朴的 listXml 操纵添加到 UserController,如清单 14 所示。( 不要健忘导入 grails.converters 包)。

清单 24. 节制器中的简朴 XML 输出

import  grails.converters.*
class UserController {
  def listXml = {
   render User.list() as XML
  }

  // snip...
}

然后将一个 testListXml() 要领添加到 UserControllerTests.groovy,如 清单 25 所示:

清单 25. 测试 XML 输出

void testListXml() {

  def suziq = new User(name:"Suzi Q",
            login:"suziq",
            password:"wordpass",
            role:"admin")

  mockDomain(User, [suziq])

  controller.listXml()
  def xml = controller.response.contentAsString
  def list = new XmlParser().parseText(xml)
  assertEquals "suziq", list.user.login.text()

  //output
  /*
  <?xml version="1.0" encoding="UTF-8"?>
  <list>
   <user>
    <class>User</class>
    <id>1</id>
    <login>suziq</login>
    <name>Suzi Q</name>
    <password>wordpass</password>
    <role>admin</role>
    <version />
   </user>
  </list>
  */
}

此测试中产生的第一件事是,建设一个新 User 并将其存储在 suziq 变量中 ,接下来,模仿 User 表,将 suziq 存储为独一的记录。

当根基配置完成之后,挪用 listXml() 操纵。要以 String 的形式从操纵获 取生成的 XML,挪用 controller.response.contentAsString 并将其存储在 xml 变量中。

此刻,您拥有了一个原始 String。(此 String 的内容仅用于在要领末端的 output 注释中引用)。挪用 new XmlParser().parseText(xml) 会以 groovy.util.Node 工具的形式返回根元素 (<list>)。一旦拥有了 XML 文档的根节点,您就可以利用 GPath 表达式(譬喻 list.user.login.text()) 来断言,<login> 元素包括预期的值(在本例中为 suziq)。

可以看到,Grails converters 包简化了 XML 的生成进程,本机 Groovy 库 XmlParser 简化了 XML 的理会进程,而 ControllerUnitTestCase 简化了测试 功效 GrailsMockHttpServletResponse 的进程。这是一个强大的技能组合,使 得只需短短几行代码就可以举办测试。

竣事语

在本文中,您进修了内置的测试类 GrailsUnitTestCase 和 ControllerUnitTestCase,它们大大简化了 Grails 应用措施的测试。 mockForConstraintsTests()、mockDomain() 和 mockLogging() 要领支持编写 更快的单位测试来取代迟钝的集成测试,从而显著提高应用措施开拓速度。

在下一期中,我将先容社区提供的一些测试插件,这些插件可以或许简化集成测 试。届时请继承享受能干 Grails 带来的兴趣吧。

本文配套源码

    关键字:

在线提交作业