Grails 测试指南

基础知识概要html

测试分类
    黑盒测试:没法打开的黑盒,测试人员不考虑任何内部逻辑,按照软件设计说明书编写测试用例进行测试,保证程序准确无误运行(测试人员)
    白盒测试:借助程序内部逻辑相关信息,检测内部逻辑动做是否按照软件设计说明书要求进行,检测每条通路是否工做正常,是从程序结构层面出发对软件进行测试(开发人员)
 
测试方法
    单元测试
  • 单元测试是单元级别的测试,测试单个方法或代码块,不考虑周围的基础结构
  • 单元测试一般在没有涉及 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 时须要注意
一、要配置"方言"为 "org.hibernate.dialect.MySQL5InnoDBDialect",不然 @Rollback 注解无效。
二、测试时,在同一事物中的操做,会被回滚。可是配置了 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
:mergeTestReports FAILURE: 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 功能测试
功能测试涉及针对正在运行的应用程序发出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
相关文章
相关标签/搜索