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
相關文章
相關標籤/搜索