Grails 测试指南

基础知识概要

测试分类
    黑盒测试:无法打开的黑盒,测试人员不考虑任何内部逻辑,按照软件设计说明书编写测试用例进行测试,保证程序准确无误运行(测试人员)
    白盒测试:借助程序内部逻辑相关信息,检测内部逻辑动作是否按照软件设计说明书要求进行,检测每条通路是否工作正常,是从程序结构层面出发对软件进行测试(开发人员)
 
测试方法
    单元测试
  • 单元测试是单元级别的测试,测试单个方法或代码块,不考虑周围的基础结构
  • 单元测试通常在没有涉及 I/O 数据库、资源文件等的情况下进行测试,需要快速反馈
    集成测试
  • 对组装起来的程序模块进行测试
  • 可以完全运用系统环境执行测试
    功能测试
  • 对正在运行的应用程序发出请求,验证其结果和行为
 
系统环境
Windows 10
JDK 8
IDEA 2019.1
Grails 3.3.0
 
 
Grails 项目构建
从导入并启用 grails 框架开始。首先需要下载 grails SDK,并保存到本地目录,本机保存位置是 C:\grails-3.3.0
 
使用 IDEA 新建项目,create-app --> next
 
 
填写 project name 和 project localtion ,finish 
 
 
grails 应用程序会通过 gradle 进行构建操作,下图为构建成功
 
 
 如果打开项目目录,发现未识别 domain、controller 等目录(未变蓝色),则重新点击  再次构建。
 
grails 项目构建成功,IDEA 会自动识别 grails 应用程序的目录,这归功于 grails "约定大于配置"的格言。一个启用好的 grails 项目如图
 
 
 
 
Grails 测试配置
要开始测试,需要导入入 grails 测试依赖
 
找到项目的 build.gradle 文件中的  dependencies 闭包,通常闭包中已经导入必要的依赖包。以下是 grails 3.3.0 自动导入的依赖项
 
 
Grails 官方测试文档中,展示了如下两个依赖
 
// grails 测试框架
// Documnet : https://testing.grails.org/latest/guide/index.html
testRuntime "org.grails:grails-gorm-testing-support:1.1.5"
testRuntime "org.grails:grails-web-testing-support:1.1.5"
 
使用官方SDK默认配置,或者官方文档中推荐配置,都可以引入测试框架。
 
 
Grails 测试准备
点击  启动项目,如果输出
 
"C:\Program Files\Java\jdk1.8.0_102\bin\java.exe" -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:CICompilerCount=3 -Djline.WindowsTerminal.directConsole=false -Dgrails.full.stacktrace=true -Dfile.encoding=UTF-8 -classpath C:\Users\codingR\AppData\Local\Temp\classpath672624394.jar org.grails.cli.GrailsCli run-app --plain-output
|Running application...
Listening for transport dt_socket at address: 5954
Connected to the target VM, address: '127.0.0.1:5954', transport: 'socket'
Grails application running at http://localhost:8080 in environment: development
 
 
 
说明 grails 项目搭建好了,可以进行下面的操作。
 
以下篇幅中使用的是 grails 官方自带的开源内存数据库 h2,只要完成上面项目的配置,就可以直接启动项目,并执行 CRUD。
 
CRUD 操作全部由 h2 和 hibernate 支持,所有操作都在内存中执行,当重启项目后,内存中的“持久化”操作数据会被清空。
 
数据库配置位置在 application.yml 文件中
 
---
hibernate:
    cache:
        queries: false
        use_second_level_cache: false
        use_query_cache: false
dataSource:
    pooled: true
    jmxExport: true
    logSql: true
    driverClassName: org.h2.Driver
    username: sa
    password: ''
 
 
environments:
    development:
        dataSource:
            dbCreate: create-drop
            url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    test:
        dataSource:
            dbCreate: update
            url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    production:
        dataSource:
            dbCreate: none
            url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
            properties:
                jmxEnabled: true
                initialSize: 5
                maxActive: 50
                minIdle: 5
                maxIdle: 25
                maxWait: 10000
                maxAge: 600000
                timeBetweenEvictionRunsMillis: 5000
                minEvictableIdleTimeMillis: 60000
                validationQuery: SELECT 1
                validationQueryTimeout: 3
                validationInterval: 15000
                testOnBorrow: true
                testWhileIdle: true
                testOnReturn: false
                jdbcInterceptors: ConnectionState
                defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED
 
如果想使用 MySQL 进行测试,更改数据库配置
 
---
hibernate:
    cache:
        queries: false
        use_second_level_cache: false
        use_query_cache: false
dataSource:
    pooled: true
    jmxExport: true
    logSql: true
#    driverClassName: org.h2.Driver
#    username: sa
#    password: ''
    driverClassName: com.mysql.jdbc.Driver
    username: #{you database username}
    password: #{you database password}
    dialect: org.hibernate.dialect.MySQL5InnoDBDialect
 
 
environments:
    development:
        dataSource:
            dbCreate: create-drop
#            url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
            url: jdbc:mysql://localhost:3306/testdemo?autoReconnect=true&characterEncoding=utf-8&useSSL=false
    test:
        dataSource:
            dbCreate: update
#            url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
            url: jdbc:mysql://localhost:3306/testdemo?autoReconnect=true&characterEncoding=utf-8&useSSL=false
    production:
        dataSource:
            dbCreate: none
#            url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
            url: jdbc:mysql://localhost:3306/testdemo?autoReconnect=true&characterEncoding=utf-8&useSSL=false
            properties:
                jmxEnabled: true
                initialSize: 5
                maxActive: 50
                minIdle: 5
                maxIdle: 25
                maxWait: 10000
                maxAge: 600000
                timeBetweenEvictionRunsMillis: 5000
                minEvictableIdleTimeMillis: 60000
                validationQuery: SELECT 1
                validationQueryTimeout: 3
                validationInterval: 15000
                testOnBorrow: true
                testWhileIdle: true
                testOnReturn: false
                jdbcInterceptors: ConnectionState
                defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED
 
使用 MySQL 时需要注意
1、要配置"方言"为 "org.hibernate.dialect.MySQL5InnoDBDialect",否则 @Rollback 注解无效。
2、测试时,在同一事物中的操作,会被回滚。但是配置了 dbCreate: create-drop,Grails 会默认创建表结构,但是 @Rollback 对创建过程无效,即不会回滚创建出来的表。
 
从 Grails 单元测试开始
在项目 controller 文件夹上右键,新建一个 DemoController
 
 
package com.rishiqing.demo
 
class DemoController {
 
    def index() { }
}
 
grails 会在创建 DemoController 后,为你在 src/test/groovy/com/rishiqing/demo 路径下创建一个单元测试类,这是 grails 框架自动完成的操作
 
package com.rishiqing.demo
 
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification
 
class DemoControllerSpec extends Specification implements ControllerUnitTest<DemoController> {
 
    def setup() {
    }
 
    def cleanup() {
    }
 
    void "test something"() {
        expect:"fix me"
            true == false
    }
}
 
在 DemoController 中编写业务逻辑,并重新编写 DemoControllerSpec 类完成一个简单的单元测试。添加接口
 
package com.rishiqing.demo
 
class DemoController {
 
    def renderHello () {
        render status:200, text :"hello"
    }
}
 
在 DemoControllerSpec 中添加单元测试
 
package com.rishiqing.demo
 
 
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification
 
 
class DemoControllerSpec extends Specification implements ControllerUnitTest<DemoController> {
 
    void "test renderHello function"() {
        when :
        controller.renderHello()
        then :
        status == 200
        response.text == "hello1" // 测试错误的结果
    }
}
 
如果测试异常,则 console 会提示测试结果和测试失败的信息,并生成一个错误信息页面,可以使用浏览器打开 D:/proj/testDemo/build/reports/tests/test/index.html 位置的错误页面
 
Testing started at 19:29 ...
"C:\Program Files\Java\jdk1.8.0_102\bin\java.exe" -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:CICompilerCount=3 -Dgrails.full.stacktrace=true -Djline.WindowsTerminal.directConsole=false -Dfile.encoding=UTF-8 -classpath C:\Users\codingR\AppData\Local\Temp\classpath1646263186.jar org.grails.cli.GrailsCli intellij-command-proxy test-app com.rishiqing.demo.DemoControllerSpec -unit -echoOut --plain-output
:compileJava NO-SOURCE
:compileGroovy UP-TO-DATE
:buildProperties UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava NO-SOURCE
:compileTestGroovy UP-TO-DATE
:processTestResources NO-SOURCE
:testClasses UP-TO-DATE
:testListening for transport dt_socket at address: 8216
Connected to the target VM, address: '127.0.0.1:8216', transport: 'socket'
 
 
Condition not satisfied:
 
 
response.text == "hello1"
|        |    |
|        |    false
|        |    1 difference (83% similarity)
|        |    hello(-)
|        |    hello(1)
|        hello
org.grails.plugins.testing.GrailsMockHttpServletResponse@3bdf09f9
 
 
Condition not satisfied:
 
 
response.text == "hello1"
|        |    |
|        |    false
|        |    1 difference (83% similarity)
|        |    hello(-)
|        |    hello(1)
|        hello
org.grails.plugins.testing.GrailsMockHttpServletResponse@3bdf09f9
 
 
    at com.rishiqing.demo.DemoControllerSpec.test renderHello function(DemoControllerSpec.groovy:13)
 
 
com.rishiqing.demo.DemoControllerSpec > test renderHello function FAILED
    org.spockframework.runtime.SpockComparisonFailure at DemoControllerSpec.groovy:13
Disconnected from the target VM, address: '127.0.0.1:8216', transport: 'socket'
1 test completed, 1 failed
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///D:/proj/testDemo/build/reports/tests/test/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
:test FAILED
BUILD FAILED
Total time: 8.358 secs
Tests FAILED |
Test execution failed
 
 
Process finished with exit code 1
 
 
 
 
 
Grails 集成测试
grails 用命令创建一个集成测试
 
$ grails create-integration-test [路径名 + 测试文件名]
 
创建一个集成测试,集成测试创建好后,在 src/integration-test 目录下
 
Microsoft Windows [版本 10.0.17763.503]
(c) 2018 Microsoft Corporation。保留所有权利。
 
 
D:\proj\testDemo>grails create-integration-test com.rishiqing.demo.DemoControllerIntegration
| Created src/integration-test/groovy/com/rishiqing/demo/DemoControllerIntegrationSpec.groovy
D:\proj\testDemo>
 
 
 
 

 

打开集成测试文件,和刚才 grails 自动创建的单元测试文件比较,发现只多了 @Integration 和 @Rollback 注解,并取消了单元测试的实现
 
单元测试文件
 
package com.rishiqing.demo
 
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification
 
class DemoControllerSpec extends Specification implements ControllerUnitTest<DemoController> {
 
    def setup() {
    }
 
    def cleanup() {
    }
 
    void "test something"() {
        expect:"fix me"
        true == false
    }
}
 
集成测试文件
 
package com.rishiqing.test
 
import grails.testing.mixin.integration.Integration
import grails.transaction.*
import spock.lang.Specification
 
@Integration
@Rollback
class DemoControllerIntegrationSpec extends Specification {
 
    def setup() {
    }
 
    def cleanup() {
    }
 
    void "test something"() {
        expect:"fix me"
            true == false
    }
}
 
 
实际应用
在实际的 web 应用中,单元测试使用较少,更多的是集成测试。
使用单元测试的位置,比如 Util 模块中,计算日期的方法,可能需要使用到单元测试。
在 web 应用中,由于需要各个模块、层、接口之间协同工作,而且需要用到数据库 I/O 资源,因此实际项目中使用更多的是集成测试。
 
准备工作
在 domain 中创建一个 User 领域类和 Team 领域类
 
package com.rishiqing.test
 
class User {
    String email
    String nickName
 
    static belongsTo = [
            team : Team
    ]
}
 
 
package com.rishiqing.test
 
class Team {
    String name
    String logoUrl
    String password
 
    static hasMany = [
            user : User
    ]
}
 
创建 UserDaoService ,并对 User 的 DAO 层进行编码
 
package com.rishiqing.test.dao
 
import com.rishiqing.test.Team
import com.rishiqing.test.User
import com.rishiqing.test.dto.UserDTO
import grails.gorm.transactions.Transactional
 
@Transactional
class UserDaoService {
 
    def getById (Long id) {
        User.findById(id)
    }
 
    def getByEmail (String email) {
        (User) User.createCriteria().get {
            eq "email", email
        }
    }
 
    def listByTeam (Team team) {
        (List<User>) User.createCriteria().list {
            eq "team", team
        }
    }
 
    def save(User user) {
        user.save()
    }
 
    def remove (User user) {
        user.delete()
    }
}
 
grails 框架自动生成的 UserDaoServiceSpec 单元测试文件
 
package com.rishiqing.test
 
import com.rishiqing.test.dao.UserDaoService
import grails.testing.services.ServiceUnitTest
import spock.lang.Specification
 
class UserDaoServiceSpec extends Specification implements ServiceUnitTest<UserDaoService>{
 
    def setup() {
    }
 
    def cleanup() {
    }
 
    void "test something"() {
        expect:"fix me"
            true == false
    }
}
 
通过命令,手动在相应位置创建集成测试文件(使用 IDEA Teminal 选项卡中的命令行)
 
D:\proj\testDemo>grails create-integration-test com.rishiqing.test.UserDaoServiceIntegration
| Created src/integration-test/groovy/com/rishiqing/test/UserDaoServiceIntegrationSpec.groovy
D:\proj\testDemo>
 
 
 
创建完成得 UserDaoServiceIntegrationSpec 集成测试文件
 
package com.rishiqing.demo
 
import grails.testing.mixin.integration.Integration
import grails.transaction.*
import spock.lang.Specification
 
@Integration (1)
@Rollback (2)
class DemoControllerIntegrationSpec extends Specification {
 
    def setup() {    (3)
    }
 
    def cleanup() {
    }
 
    void "test something"() {
        expect:"fix me"
            true == false
    }
}
 
(1)使用 Integration 注解表示这是一个集成测试
(2)使用 Rollback 注解可以确保每个测试方法,在被回滚的事务中运行,而不会插入到数据库中
(3) setup 方法
  • 负责初始化操作,不需要初始化操作,可以移除
  • 在每个测试方法执行时都会被重新调用
  • 不同于编写的每一个测试方法,setup 方法使用了单独事务。即使添加了 Rollback 注解,在 setup 方法中进行持久化操作,也会被持久化到数据库
 
测试流程
编写一个集成测试案例,测试 UserDaoService 中的 getById 方法
 
package com.rishiqing.test
 
import com.rishiqing.test.dao.UserDaoService
import grails.testing.mixin.integration.Integration
import grails.transaction.*
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
 
@Integration
@Rollback
class UserDaoServiceIntegrationSpec extends Specification {
 
    @Autowired (1)
    UserDaoService userDaoService
 
    void "test getById function"() {
        when : (2)
        def user = userDaoService.getById(1.toLong())
        then: (3)
        user == null
    }
}
 
(1) spring beans 的自动注入,自动注入 userDaoService
(2) 先决条件
(3) 预测结果
 
点击执行测试按钮开始测试。
注意,选择测试时,会有两个选项,Grails 测试和 JUnit 测试。
因为 Grails 测试是基于 JUnit 测试的,因此会引入 JUnit 依赖,IDEA 在运行测试时,会检测本项目支持的测试框架,所以会有两个选项。
但是不能选择 JUnit 测试,相对于 Grails ,JUnit 测试缺少 GROM 环境,无法执行 Grails 集成测试。会出现:
 
java.lang.IllegalStateException: No GORM implementations configured. Ensure GORM has been initialized correctly
 
异常,导致测试无法执行。因此需要选择 Grails 测试。
 
 
如果错误的选择了 JUnit 测试,请在 IDEA 编辑项目配置位置删除这个配置,重新选择,并执行测试。
 

 

在类左侧点执行按钮会执行此测试类中所有的测试方法
在方法左侧点击执行按钮,只会执行本测试方法
测试成功后,按钮会变为对勾状态
测试失败后,按钮会出现异常状
 
测试 getById 方法
 
class UserDaoServiceIntegrationSpec extends Specification {
    ...
    void "test getById function"() {
        when : 
        def user = userDaoService.getById(1.toLong())
        then: 
        user == null
    }
    ...
}
 
一个正常的测试结果
 
Testing started at 16:30 ...
"C:\Program Files\Java\jdk1.8.0_102\bin\java.exe" -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:CICompilerCount=3 -Dgrails.full.stacktrace=true -Djline.WindowsTerminal.directConsole=false -Dfile.encoding=UTF-8 -classpath C:\Users\codingR\AppData\Local\Temp\classpath1263477126.jar org.grails.cli.GrailsCli intellij-command-proxy test-app com.rishiqing.test.UserDaoServiceIntegrationSpec -integration -echoOut --plain-output
|Resolving Dependencies. Please wait...
CONFIGURE SUCCESSFUL
Total time: 4.393 secs
:compileJava NO-SOURCE
:compileGroovy:buildProperties:processResources:classes
:compileTestJava NO-SOURCE
:compileTestGroovy:processTestResources NO-SOURCE
:testClasses
:compileIntegrationTestJava NO-SOURCE
:compileIntegrationTestGroovy:processIntegrationTestResources NO-SOURCE
:integrationTestClasses
:integrationTestListening for transport dt_socket at address: 13129
Connected to the target VM, address: '127.0.0.1:13129', transport: 'socket'
Grails application running at http://localhost:13189 in environment: test
Disconnected from the target VM, address: '127.0.0.1:13129', transport: 'socket'
:mergeTestReportsBUILD SUCCESSFUL
Total time: 21.018 secs
|Tests PASSED
 
 
Process finished with exit code 0
 
 
改动,测试一个异常的测试结果
 
class UserDaoServiceIntegrationSpec extends Specification {
    ...
    void "test getById function"() {
        when :
        def user = userDaoService.getById(1.toLong())
        then:
        user != null
    }
    ...
}
 
一个异常的测试结果
 
Testing started at 16:31 ...
"C:\Program Files\Java\jdk1.8.0_102\bin\java.exe" -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:CICompilerCount=3 -Dgrails.full.stacktrace=true -Djline.WindowsTerminal.directConsole=false -Dfile.encoding=UTF-8 -classpath C:\Users\codingR\AppData\Local\Temp\classpath955494352.jar org.grails.cli.GrailsCli intellij-command-proxy test-app com.rishiqing.test.UserDaoServiceIntegrationSpec -integration -echoOut --plain-output
:compileJava NO-SOURCE
:compileGroovy UP-TO-DATE
:buildProperties UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava NO-SOURCE
:compileTestGroovy UP-TO-DATE
:processTestResources NO-SOURCE
:testClasses UP-TO-DATE
:compileIntegrationTestJava NO-SOURCE
:compileIntegrationTestGroovy:processIntegrationTestResources NO-SOURCE
:integrationTestClasses
:integrationTestListening for transport dt_socket at address: 13262
Connected to the target VM, address: '127.0.0.1:13262', transport: 'socket'
Grails application running at http://localhost:13306 in environment: test
 
 
Condition not satisfied:
 
 
user != null
|    |
null false
 
 
Condition not satisfied:
 
 
user != null
|    |
null false
 
 
    at com.rishiqing.test.UserDaoServiceIntegrationSpec.$tt__$spock_feature_0_0(UserDaoServiceIntegrationSpec.groovy:20)
    at com.rishiqing.test.UserDaoServiceIntegrationSpec.test something_closure1(UserDaoServiceIntegrationSpec.groovy)
    at groovy.lang.Closure.call(Closure.java:414)
    at groovy.lang.Closure.call(Closure.java:430)
    at grails.gorm.transactions.GrailsTransactionTemplate$1.doInTransaction(GrailsTransactionTemplate.groovy:68)
    at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:133)
    at grails.gorm.transactions.GrailsTransactionTemplate.executeAndRollback(GrailsTransactionTemplate.groovy:65)
    at com.rishiqing.test.UserDaoServiceIntegrationSpec.test something(UserDaoServiceIntegrationSpec.groovy)
 
 
com.rishiqing.test.UserDaoServiceIntegrationSpec > test something FAILED
    org.spockframework.runtime.ConditionNotSatisfiedError at UserDaoServiceIntegrationSpec.groovy:20
Disconnected from the target VM, address: '127.0.0.1:13262', transport: 'socket'
1 test completed, 1 failed
:integrationTest FAILED
:mergeTestReportsFAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':integrationTest'.
> There were failing tests. See the results at: file:///D:/proj/testDemo/build/test-results/integrationTest/
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED
Total time: 17.731 secs
Tests FAILED |
Test execution failed
 
 
Process finished with exit code 1
 
 
 
完善测试
完善 UserDaoServiceIntegrationSpec 对 UserDaoService 的所有方法的测试
 
package com.rishiqing.test.dao
 
import com.rishiqing.test.Team
import com.rishiqing.test.User
import com.rishiqing.test.dao.UserDaoService
import grails.testing.mixin.integration.Integration
import grails.transaction.*
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
 
@Integration
@Rollback
class UserDaoServiceIntegrationSpec extends Specification {
 
    @Autowired
    UserDaoService userDaoService
 
    void "test getById function"() {        (1)
        given: "初始化测试数据"              (2)
        def team = new Team()
        team.name = '公司'
        team.logoUrl = 'http://www.rishiqing.com'
        team.save()
 
        def user = new User()
        user.nickName = '小明'
        user.email = '1@rishiqing.com'
        user.password = '123456'
        user.team = team
        user.save()
 
        when:
        def instance = userDaoService.getById(user.id)
 
        then:
        user.id == instance.id
    }
 
    void "test getByEmail function" () {
        given:
        def team = new Team()
        team.name = '公司'
        team.logoUrl = 'http://www.rishiqing.com'
        team.save()
 
        def user = new User()
        user.nickName = '小明'
        user.email = '1@rishiqing.com'
        user.password = '123456'
        user.team = team
        user.save()
 
        when:
        def instance = userDaoService.getByEmail(user.email)
 
        then:
        instance.email == user.email
    }
 
    void "test listByTeam function" () {
        given:
        def team = new Team()
        team.name = '公司'
        team.logoUrl = 'http://www.rishiqing.com'
        team.save()
 
        def user = new User()
        user.nickName = '小明'
        user.email = '1@rishiqing.com'
        user.password = '123456'
        user.team = team
        user.save()
 
        when:
        def users = userDaoService.listByTeam(team)
 
        then:
        users.size() == 1
    }
 
    void "test save function" () {
        given:
        def team = new Team()
        team.name = '公司'
        team.logoUrl = 'http://www.rishiqing.com'
        team.save()
 
        when:
        def user = new User()
        user.nickName = '小明'
        user.email = '1@rishiqing.com'
        user.password = '123456'
        user.team = team
        def instance = userDaoService.save(user)
 
        then:
        instance.email == '1@rishiqing.com'
    }
 
    void "test remove function" () {
        given:
        def team = new Team()
        team.name = '公司'
        team.logoUrl = 'http://www.rishiqing.com'
        team.save()
 
        def user = new User()
        user.nickName = '小明'
        user.email = '1@rishiqing.com'
        user.password = '123456'
        user.team = team
        user.save()
 
        when:
        userDaoService.remove(user)
 
        then:
        User.findByEmail("1@qq.com") == null
    }
 
}
 
(1) 可以更改测试方法名,对需要测试的方法进行说明
(2) given 关键字用来描述测试数据,when 关键字用来描述测试方法,then 关键字用来描述测试结果
冒号后面可以通过  "" 的方式添加说明
 
在编写测试的环节中,需要注意被测试方法的唯一性。测试对应方法时,不要引入别的方法。
例如在测试 getById 方法时,需要先创建一个 User 对象,通过 User 对象的 id 测试 getById 方法。
在创建对象这一步,可以使用 new User().save(),也可以使用 userDaoService.save(new User()),显然我们不应该使用第二种方法,因为会对要测试的方法 getById 产生干扰。
 
更为复杂的
按照 web 应用开发的规范,为项目分层
 
DAO --> Manager --> Service --> Controller
        |                       |
        DTO                     VO
 
DAO 数据业务层,数据库访问,查询
Manager 公共业务层,对接 DO 和 DTO,处理转换及业务处理
Service 服务层,web 业务
Controller 控制器,接口
 
DTO 数据传输对象,业务层数据传输
VO 视图对象,前端展示
 
使用上述分层,对用户业务逐层编码并进行测试。为流程需要,先创建 UserDTO,UserVO ,用作用户的数据传输和前端视图
 
UserDTO 
 
package com.rishiqing.test.dto
 
class UserDTO {
    Long id
    String email
    String nickName
    Long teamId
    String teamName
    String teamLogoUrl
}
 
UserVO 
 
package com.rishiqing.test.vo
 
import com.rishiqing.test.dto.UserDTO
 
class UserVO {
 
    static toMap (UserDTO userDTO) {
        [
                id : userDTO.id,
                nickName: userDTO.nickName,
                email: userDTO.email
        ]
    }
}
 
 
Manager 层测试
编写一个获取用户的方法,可以通过 id 和 email 进行获取,且返回给上层 DTO 对象
 
package com.rishiqing.test.manager
 
import com.rishiqing.test.dto.UserDTO
import com.rishiqing.test.exception.ParamNotFoundException
import grails.gorm.transactions.Transactional
 
@Transactional
class UserManagerService {
 
    def userDaoService
 
    def getUser(UserDTO userDTO) {
        def instance
        if (userDTO.id) {
            instance = userDaoService.getById(userDTO.id)
        } else if (userDTO.email) {
            instance = userDaoService.getByEmail(userDTO.email)
        } else {
            throw new ParamNotFoundException()
        }
        userDTO.id = instance.id
        userDTO.nickName = instance.nickName
        userDTO.email = instance.email
 
        return userDTO
    }
 
}
 
Manager 层的集成测试,需要使用到 DAO 层模块协同。测试文件 UserManagerServiceIntegrationSpec
 
package com.rishiqing.test.manager
 
import com.rishiqing.test.Team
import com.rishiqing.test.User
import com.rishiqing.test.dao.UserDaoService
import com.rishiqing.test.dto.UserDTO
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
 
@Integration
@Rollback
class UserManagerServiceIntegrationSpec extends Specification {
 
    @Autowired
    UserManagerService userManagerService
 
    @Autowired
    UserDaoService userDaoService
 
    def setup () {
        userManagerService.userDaoService = this.userDaoService  (1)
    }
 
    void "test getUser function" () {
 
        def team = new Team(                  
                name: "公司",
                logoUrl: "https://www.rishiqing.com",
        ).save()
        def user = new User(
                nickName: "小红",
                email: "2@rishiqing.com",
                password: 123456,
                team: team
        ).save()
 
        when: "测试通过 id 获取 DTO"
        UserDTO userDTO = new UserDTO()
        userDTO.id = user.id
        userDTO = userManagerService.getUser(userDTO)
 
        then:
        userDTO.id == user.id
        userDTO.nickName == user.nickName
        userDTO.email == user.email
 
        when: "测试通过 email 获取 DTO"              (2)
        UserDTO userDTO1 = new UserDTO()
        userDTO1.email = user.email
        userDTO1 = userManagerService.getUser(userDTO1)
 
        then:
        userDTO1.id == user.id
        userDTO1.nickName == user.nickName
        userDTO1.email == user.email
 
    }
}
 
(1) 需要给 userManagerService 中的 userDaoService 赋值
(2) 当有多个测试流程时,可以分写成多个 when ... then ... 添加更多的测试流程
 
对于 setup 方法,它适合做一些注入类初始化操作,并不适合在其中执行数据持久化操作,因为无法回滚。
推荐的方式是在每个测试中执行数据持久化操作,又或者提出公共方法,让每个测试方法调用公共方法,例如上述测试可以改写为
 
package com.rishiqing.test.manager
 
import com.rishiqing.test.Team
import com.rishiqing.test.User
import com.rishiqing.test.dao.UserDaoService
import com.rishiqing.test.dto.UserDTO
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
 
 
@Integration
@Rollback
class UserManagerServiceIntegrationSpec extends Specification {
 
 
    @Autowired
    UserManagerService userManagerService
 
    @Autowired
    UserDaoService userDaoService
 
    def setup () {
        userManagerService.userDaoService = this.userDaoService
    }
 
    def initTestData () {
        def team = new Team(
                name: "公司",
                logoUrl: "https://www.rishiqing.com",
        ).save()
        def user = new User(
                nickName: "小红",
                email: "2@rishiqing.com",
                password: 123456,
                team: team
        ).save()
        return [team, user]
    }
 
    void "test getUser function" () {
        given:
        def (team, user) = initTestData()
 
        when: "测试通过 id 获取 DTO"
        UserDTO userDTO = new UserDTO()
        userDTO.id = user.id
        userDTO = userManagerService.getUser(userDTO)
 
        then:
        userDTO.id == user.id
        userDTO.nickName == user.nickName
        userDTO.email == user.email
 
        when: "测试通过 email 获取 DTO"
        UserDTO userDTO1 = new UserDTO()
        userDTO1.email = user.email
        userDTO1 = userManagerService.getUser(userDTO1)
 
        then:
        userDTO1.id == user.id
        userDTO1.nickName == user.nickName
        userDTO1.email == user.email
 
    }
}
 
Service 层测试
 
userService 层编码大同小异,需要多注入两个 service
 
package com.rishiqing.test.service
 
import com.rishiqing.test.dto.UserDTO
import com.rishiqing.test.exception.DataNotFoundException
import com.rishiqing.test.exception.ParamNotFoundException
import grails.gorm.transactions.Transactional
import grails.web.servlet.mvc.GrailsParameterMap
 
@Transactional
class UserService {
 
    def userManagerService
 
    def getUser(GrailsParameterMap params) {
        if (!params.get("email")) {
            throw new ParamNotFoundException("email")
        }
        UserDTO userDTO = new UserDTO()
        userDTO.email = params.get("email")
        userDTO = userManagerService.getUser(userDTO)
        if (!userDTO) {
            throw new DataNotFoundException("user")
        }
 
        return userDTO
    }
}
 
userServiceIntegrationSpec 文件编码
 
package com.rishiqing.test.service
 
import com.rishiqing.test.Team
import com.rishiqing.test.User
import com.rishiqing.test.dao.UserDaoService
import com.rishiqing.test.manager.UserManagerService
import grails.testing.mixin.integration.Integration
import grails.transaction.*
import grails.web.servlet.mvc.GrailsParameterMap
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
 
@Integration
@Rollback
class UserServiceIntegrationSpec extends Specification {
 
    @Autowired
    UserService userService
 
    @Autowired
    UserManagerService userManagerService
 
    @Autowired
    UserDaoService userDaoService
 
    def setup () {
        userService.userManagerService = this.userManagerService
        userService.userManagerService.userDaoService = this.userDaoService
    }
 
    def initTestData () {
        def team = new Team(
                name : '公司',
                logoUrl: "https://www.rishiqing.com"
        ).save()
        def user = new User(
                nickName: "小李",
                email: '3@rishiqing.com',
                password: 123456,
                team: team
        ).save()
        return [team, user]
    }
 
    void "test getUser function"() {
        given: "初始化测试数据"
        initTestData()
 
        when:
        def params = new GrailsParameterMap([
                email: "3@rishiqing.com"
        ],null)
        def userDTO = userService.getUser(params)
 
        then:
        params.email == userDTO.email
    }
}
 
Controller 层测试
controller 层测试,需要在引入集成测试的同时,实现单元测试框架。
实现单元测试框架后,就可以使用 params ,request ,response 等公共变量,舍去很多 grails 框架为 controller 做的初始化操作,可以直接进入测试流程。
 
@Integration
@Rollback
class UserControllerIntegrationSpec extends Specification implements ControllerUnitTest<UserController> { 
    ...
}
 
UserController 控制器
 
package com.rishiqing.test
 
import com.rishiqing.test.exception.ServerException
import com.rishiqing.test.vo.UserVO
import grails.converters.JSON
 
class UserController {
 
    def userService
 
    def show() {
        try {
            def userDTO = userService.getUser(params)
            render UserVO.toMap(userDTO) as JSON
        } catch (Exception e) {
            log.error("系统错误" + e.message)
            def se = new ServerException()
            render se.renderMap as JSON
        }
    }
}
 
UserControllerIntegrationSpec 测试文件
 
package com.rishiqing.test
 
import com.rishiqing.test.dao.UserDaoService
import com.rishiqing.test.manager.UserManagerService
import com.rishiqing.test.service.UserService
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
import grails.testing.web.controllers.ControllerUnitTest
import org.grails.web.json.JSONObject
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
 
@Integration
@Rollback
class UserControllerIntegrationSpec extends Specification implements ControllerUnitTest<UserController> {
 
    @Autowired
    UserService userService
 
    @Autowired
    UserManagerService userManagerService
 
    @Autowired
    UserDaoService userDaoService
 
    def setup () {
        controller.userService = this.userService     (1)
        userService.userManagerService = this.userManagerService
        userService.userManagerService.userDaoService = this.userDaoService
    }
 
    def initTestData () {
        def team = new Team(
                name : '公司',
                logoUrl: "https://www.rishiqing.com"
        ).save()
        def user = new User(
                nickName: "小赵",
                email: '4@rishiqing.com',
                password: 123456,
                team: team
        ).save()
        return [team, user]
    }
 
    void "test userController getUser interface" () {
        given:
        initTestData()
 
        when:
        params.email = "4@rishiqing.com"                (2)
        controller.show()                               (3) 
 
        then:
        JSONObject o = new JSONObject(response.json.toString())        (4)
        o.email == params.get("email")
    }
}
 
(1) 可以直接使用公共变量 controller,此 controller 指代 implements ControllerUnitTest<UserController> 中的 UserController
(2) 请求参数Map GrailsParameterMap 也可以作为公共变量使用
(3) 直接调用 show() 接口,会自动引入 params 变量,来模拟请求
(4) 使用公共变量 response,可以获取 show() 接口 render 的数据,来验证数据
 
 
Grails 异常抛出测试
对于接口与方法中的异常,grails 在 spock.lang.Specification
中提供了处理方法。来看一个简单的例子:
 
获取一个字符串中的第一个字母,以下是一个正常运行的测试的 demo method
 
void "test something"() {
    when:
    String c = "Alax".charAt(0)
 
    then:
    c != null
    c == 'A'
}
 
异常检测方法:throws()
 
/**
* Specifies that the preceding <tt>when</tt> block should throw an exception.
* May only occur as the initializer expression of a typed variable declaration
* in a <tt>then</tt> block; the expected exception type is inferred from the
* variable type.
* <p>This form of exception condition is typically used if the thrown
* exception instance is used in subsequent conditions.
*
* <p>Example:
* <pre>
* when:
* "".charAt(0)
*
* then:
* IndexOutOfBoundsException e = thrown()
* e.message.contains(...)
* </pre>
*
* @return the thrown exception instance
*/
public <T extends Throwable> T thrown() {
  throw new InvalidSpecException(
      "Exception conditions are only allowed in 'then' blocks, and may not be nested inside other elements");
}
 
/**
* Specifies that the preceding <tt>when</tt> block should throw an exception
* of the given type. May only occur in a <tt>then</tt> block.
* <p>This form of exception condition is typically used if the thrown
* exception instance is <em>not</em> used in subsequent conditions.
*
* <p>Example:
* <pre>
* when:
* "".charAt(0)
*
* then:
* thrown(IndexOutOfBoundsException)
*
* @param type the expected exception type
* @param <T> the expected exception type
* @return the thrown exception instance
*/
public <T extends Throwable> T thrown(Class<T> type) {
  throw new InvalidSpecException(
      "Exception conditions are only allowed in 'then' blocks, and may not be nested inside other elements");
}
 
来测试 demo method 1,"".charAt(0) 会抛出 IndexOutOfBoundsException 异常。如果抛出的 IndexOutOfBoundsException 异常,在后续的测试业务中还需要使用它的实例 e ,那么需要采用下述写法。
 
void "test thrown() method demo 1"() {
    when:
    "".charAt(0)
 
    then:
    IndexOutOfBoundsException e = thrown()
 
    println "fix this : ${e.message}"
}
 
 
来测试 demo method 2,"".charAt(0) 会抛出 IndexOutOfBoundsException 异常。如果后续无需使用异常实例,那么采用下述写法。
 
void "test thrown() method demo 2"() {
    when:
    "".charAt(0)
 
    then:
    thrown(IndexOutOfBoundsException)
}
 
 
异常检测方法:notThrown()
 
/**
* Specifies that no exception of the given type should be
* thrown, failing with a {@link UnallowedExceptionThrownError} otherwise.
*
* @param type the exception type that should not be thrown
*/
public void notThrown(Class<? extends Throwable> type) {
  Throwable thrown = getSpecificationContext().getThrownException();
  if (thrown == null) return;
  if (type.isAssignableFrom(thrown.getClass())) {
    throw new UnallowedExceptionThrownError(type, thrown);
  }
  ExceptionUtil.sneakyThrow(thrown);
}
 
指定不应当抛出的异常类型。type 不应抛出的异常类型。如果 type 类型的异常被抛出,则测试方法会抛出 UnallowedExceptionThrownError 异常。
 
void "test notThrown() method" () {
    when:
    "".charAt(0)
 
    then:
    notThrown(IndexOutOfBoundsException)
}
 
上述方法抛出了指定的不应当抛出的异常 IndexOutOfBoundsException,则测试方法会抛出:
com.rishiqing.demo.DemoControllerIntegrationSpec > test notThrown() method FAILED
    org.spockframework.runtime.UnallowedExceptionThrownError at DemoControllerIntegrationSpec.groovy:43
        Caused by: java.lang.StringIndexOutOfBoundsException at DemoControllerIntegrationSpec.groovy:40
 
异常检测方法:noExceptionThrown()
 
/**
* Specifies that no exception should be thrown, failing with a
* {@link UnallowedExceptionThrownError} otherwise.
*/
public void noExceptionThrown() {
  Throwable thrown = getSpecificationContext().getThrownException();
  if (thrown == null) return;
  throw new UnallowedExceptionThrownError(null, thrown);
}
 
如果测试方法中不能抛出任何异常,则使用上述方法。如果测试方法检测到了任何异常,则测试会抛出:UnallowedExceptionThrownError 异常。
 
void "test noExceptionThrown() method" () {
    when:
    "".charAt(0)
 
    then:
    noExceptionThrown()
}
 
测试会抛出 UnallowedExceptionThrownError 异常:
com.rishiqing.demo.DemoControllerIntegrationSpec > test noExceptionThrown() method FAILED
    org.spockframework.runtime.UnallowedExceptionThrownError at DemoControllerIntegrationSpec.groovy:51
        Caused by: java.lang.StringIndexOutOfBoundsException at DemoControllerIntegrationSpec.groovy:48
 
 
 
最后的流程,Grails 功能测试
功能测试涉及针对正在运行的应用程序发出HTTP请求并验证结果行为。这对于端到端测试场景非常有用,例如针对JSON API进行REST调用。
使用Grails 功能测试用来验证功能的完整性。
功能而是需要在 build.gradle 导入依赖
 
dependencies {
    testCompile "org.grails:grails-datastore-rest-client"
}
 
新建 UserController 的功能测试文件
 
package com.rishiqing.test
 
import grails.gorm.transactions.Rollback
import grails.plugins.rest.client.RestBuilder
import grails.plugins.rest.client.RestResponse
import grails.testing.mixin.integration.Integration
import spock.lang.Shared
import spock.lang.Specification
 
@Integration
@Rollback
class UserControllerFunctionalSpec extends Specification{
 
    @Shared                                  (1)
    RestBuilder rest = new RestBuilder()
 
    def setup () {                          
        new User(                            (2)
                nickName: "小张",
                email: '5@rishiqing.com',
                password: 123456,
                team: new Team(
                        name : '公司',
                        logoUrl: "https://www.rishiqing.com"
                ).save()
        ).save()
    }
 
    void "test user show interface functional" () {
        given:
        String email = "5@rishiqing.com"
 
        when:
        RestResponse resp = rest.get("http://127.0.0.1:${serverPort}/user/show?email=${email}")    (3)
 
        then:
        resp.status == 200
        resp.json.size() == 3
        resp.json.email == email
 
    }
}
 
(1) Shared 注解表示此对象共享,每个测试方法不用重新创建 RestBuilder 对象
(2) 在 setup 中创建测试数据。(3) 中发送请求和当前测试属于两个不同的事务,如果使用 initTestData 的初始化方式,(3) 中的请求无法访问到数据
(3) serverPort 属性是自动注入的。它包含Grails应用程序在功能测试期间运行的任意文件
 
 
文件目录结构
 
 
 
 
参考文档
 
 
转载请注明出处 2019年7月12日16点53分 —— codingR
 

 

posted @ 2019-07-12 16:54  codingR  阅读(1082)  评论(0编辑  收藏  举报