概述###
測試是軟件成功上線的安全網。基本的測試包含單元測試、接口測試。在 “使用Groovy+Spock輕松寫出更簡潔的單測” 一文中已經討論了使用GroovySpock編寫簡潔的單測,本文講解使用Groovy+Spock來構建訂單搜索的接口測試用例集合。
主工程是用Java寫的。之所以采用Groovy, 是因為其語法近似Python的簡潔,可以方便地構造List, Map 及使用閉包方便地遍歷這些容器,可以使用元類方便地訪問Java類的私有成員。Groovy 是與 Java 系統集成的好伙伴。
接口測試用例遵循“條件-結果檢測”模式,即:給定一系列條件,調用接口返回結果,並對結果進行校驗。如果結果與條件有關聯,那么結果檢測需要將條件與結果關聯起來。接口測試用例必須是自動化可重復執行的。
對於訂單搜索來說,給定搜索條件 (CA=a, CB=b, CC=c, ......) ,調用訂單搜索接口返回的訂單列表中,應該校驗每一個訂單滿足 (RA=a, RB=b, RC=c, ......)。
出於對企業源代碼的保護,這里業務敏感的信息和代碼會略去。 僅保留與 groovy 與 可配置化相關的內容,不影響讀者理解其中的技巧。
思路及實現###
思路####
這里的關鍵點在於: 如何將訂單搜索條件與搜索結果的關聯做成可配置化的。 如果需要添加測試用例,只要增加新的配置項,而無需更改測試代碼。
訂單搜索條件,是設置 OrderSearchParam 的字段,比如 orderTypeDesc, receiverName ; 返回結果 SearchResultModel 包含一個 map[field, value] 和一個 total , map 包含每個訂單對應的字段 order_type, receiver_name 。 顯然,可以構建一個三元組 basicFieldsSearchConfig: [attributeInOrderSearchParam, returnedFieldName, testValue] 。搜索條件設置 attributeInOrderSearchParam = testValue , 然后從返回結果 map 中取出 returnedFieldName 字段,驗證其值為 testValue。 搜索設置條件 attributeInOrderSearchParam = testValue, 用到了 Groovy 元類的能力。
類 OrderSearchParam 中含有訂單搜索的各種條件字段。
@Data
@ToString(callSuper = true)
public class OrderSearchParam extends BaseParam {
private static final long serialVersionUID = 4096875864279002497L;
/** 訂單編號 */
private String orderNo;
/**
* 訂單類型
*
* 元素取自
* @see xxx.api.constants.OrderType 枚舉的 name()
* eg. NORMAL
*/
private List<String> orderTypeDesc;
/** 收貨人姓名 */
private String receiverName;
// ...
public OrderSearchParam() {}
}
代碼實現####
import org.junit.Test
import javax.annotation.Resource
class GeneralOrderSearchServiceTest extends GroovyTest {
@Resource
private GeneralOrderSearchService generalOrderSearchService
@Test
void "testSearchOrderType"() {
expect:
OrderType.values().each {
GeneralOrderSearchParam orderSearchParam = ParamUtil.buildGeneralOrderSearchParam(55)
orderSearchParam.getOrderSearchParam().setOrderTypeDesc([it.name()])
PlainResult<SearchResultModel> searchResult = generalOrderSearchService.search(orderSearchParam)
assertSearchResult(searchResult, 'order_type', it.value, orderSearchParam)
}
}
def basicFieldsSearchConfig = [
['orderNo', 'order_no', 'E201805072005xxxxxxxxx1'],
['receiverName', 'receiver_name', 'qinshu'],
// ... other test cases
]
@Test
void "testSearchBasicFields"() {
expect:
basicFieldsSearchConfig.each {
commonGeneralOrderSearchTest(it[0], it[1], it[2])
}
}
void commonGeneralOrderSearchTest(searchParamAttr, returnField, testValue) {
GeneralOrderSearchParam orderSearchParam = ParamUtil.buildGeneralOrderSearchParam(55)
OrderSearchParam searchParam = orderSearchParam.getOrderSearchParam()
searchParam.metaClass.setProperty(searchParam, searchParamAttr, testValue)
PlainResult<SearchResultModel> searchResult = generalOrderSearchService.search(orderSearchParam)
assertSearchResult(searchResult, returnField, testValue, orderSearchParam)
}
void assertSearchResult(searchResult, field, testValue, orderSearchParam) {
if (searchResult == null || searchResult.data == null || searchResult.data.total == 0) {
println(orderSearchParam)
return false
}
assertSuccess(searchResult)
SearchResultModel data = searchResult.getData()
// 通常情況下,必須保證搜索結果非空,搜索用例才有意義
assert data.total > 0
data.records.each {
rec ->
rec.get(field) == testValue
}
}
}
聯合搜索####
上面的測試用例僅考慮了單個搜索條件的情形。 現在考慮下多個搜索條件。 單個搜索條件,使用了三個元素的列表 [attributeInOrderSearchParam, returnedFieldName, testValue] 來表示;多個搜索條件,盡管可以采用列表的列表,但是不夠直觀。可以抽象成一個對象。此外,條件值與返回值不一定是相同的,需要分離出來。 單個搜索抽象為類 SingleSearchTestCase:
class SingleSearchTestCase {
String searchParamAttr
String returnField
Object condValue
Object returnValue
SingleSearchTestCase(String searchParamAttr, String returnField, Object condValue, Object returnValue) {
this.searchParamAttr = searchParamAttr
this.returnField = returnField
this.condValue = condValue
this.returnValue = returnValue
}
}
同時,搜索條件構建和檢測結果,也需要擴展成針對多個搜索條件的。 對於單個搜索條件的測試,為了保持兼容,可以做個適配函數。這樣,可以使用 List
def combinedFieldsSearchConfig = [
[new SingleSearchTestCase('receiverName', 'receiver_name', 'qinshu', 'qinshu'),new SingleSearchTestCase('orderTypeDesc', 'order_type', 'NORMAL', 0)]
]
@Test
void "testCombinedFieldsSearch"() {
expect:
combinedFieldsSearchConfig.each {
commonGeneralOrderSearchTest(it)
}
}
void commonGeneralOrderSearchTest(searchParamAttr, returnField, testValue) {
commonGeneralOrderSearchTest([new SingleSearchTestCase(searchParamAttr, returnField, testValue, testValue)])
}
void assertSearchResult(searchResult, returnField, returnValue, orderSearchParam) {
assertSearchResult(searchResult, [new SingleSearchTestCase('', returnField, '', returnValue)], orderSearchParam)
}
void commonGeneralOrderSearchTest(List<SingleSearchTestCase> testCase) {
GeneralOrderSearchParam orderSearchParam = ParamUtil.buildGeneralOrderSearchParam(55)
OrderSearchParam searchParam = orderSearchParam.getOrderSearchParam()
testCase.each {
def searchParamAttrType = searchParam.metaClass.getMetaProperty(it.searchParamAttr).type
def condValue = searchParamAttrType.equals(List.class) ? [it.condValue] : it.condValue
searchParam.metaClass.setProperty(searchParam, it.searchParamAttr, condValue)
}
PlainResult<SearchResultModel> searchResult = generalOrderSearchService.search(orderSearchParam)
assertSearchResult(searchResult, testCase, orderSearchParam)
}
void assertSearchResult(searchResult, List<SingleSearchTestCase> testCase, orderSearchParam) {
if (searchResult == null || searchResult.data == null) {
throw new AssertionError("searchParam: " + JSON.toJSONString(orderSearchParam))
}
if (searchResult.data.total == 0) {
appendFile(GlobalConstants.resultFile,
"[NotGood]testCase: " + JSON.toJSONString(orderSearchParam))
assertSuccess(searchResult)
return true
}
assertSuccess(searchResult)
SearchResultModel data = searchResult.getData()
// 通常情況下,必須保證搜索結果非空,搜索用例才有意義
appendFile(GlobalConstants.resultFile, "[Normal]testCase: " + JSON.toJSONString(orderSearchParam) + " total: " + data.total)
assert data.total > 0
data.records.each {
rec ->
testCase.each {
tc -> rec.get(tc.returnField) == tc.returnValue
}
}
}
集成Spock###
在 “使用Groovy+Spock輕松寫出更簡潔的單測” 一文中,見識了Spock簡潔優雅的語法。是否可以在接口測試用例里也使用Spock的優點呢? 只要簡單的三步即可。
引入依賴####
要在Spring工程中引入Spock依賴,並訪問Spring注入的Bean,需要同時引入 groovy-all, spock-core, spock-spring, spring-test 。如下所示。 低版本的spring-test 可能不支持。
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.7</version>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.1-groovy-2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.1-groovy-2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.9.RELEASE</version>
<scope>test</scope>
</dependency>
啟動類####
啟動類只要注明 @ContextConfiguration 的 location 即可,不需引入 RunWith。
@ContextConfiguration(locations = "classpath:applicationContext.xml")
class GroovyTest extends Specification {
@Test
void "testEmpty" () {
}
}
使用Spock####
使用Spock語法可將 testSearchBasicFields 改寫為:
@Unroll
@Test
void "testSearchBasicFields(#attr,#retField,#testValue)"() {
expect:
commonGeneralOrderSearchTest(attr, retField, testValue)
where:
attr | retField | testValue
'orderNo' | 'order_no' | 'E201805072005xxxxxxxxx1'
'receiverName' | 'receiver_name' | 'qinshu'
}
是不是更清晰明了了 ?
Groovy元類###
測試用例代碼中,使用了 Groovy元類 metaClass 。 可以看做是 Java 反射機制的語法形式的簡化。 比如可以使用 getProperty 或 setProperty 設置對象屬性的值; 可以使用 getMetaProperty 獲取對象屬性的類型,可以使用 invokeMethod 調用對象的方法等。
setProperty/getProperty 方法在配置化地構造參數對象時很有用;getMetaProperty 在根據實例成員的類型進行判斷並采取某種行為時很有用; invokeMethod 方法在java私有方法的單測中很有用。
groovy元編程的介紹可參閱文章: Groovy學習之-運行時元編程
小結###
本文講解了使用Groovy來構建訂單搜索的接口測試用例集合,並介紹了 groovy 元類的用法。 讀完本文,是否對 Groovy 的使用有了更深入的了解呢?
