使用Groovy+Spock構建可配置的訂單搜索接口測試用例集

概述

測試是軟件成功上線的安全網。基本的測試包含單元測試、接口測試。在 「使用Groovy+Spock輕鬆寫出更簡潔的單測」 一文中已經討論了使用GroovySpock編寫簡潔的單測,本文講解使用Groovy+Spock來構建訂單搜索的接口測試用例集合。html

主工程是用Java寫的。之因此採用Groovy, 是由於其語法近似Python的簡潔,能夠方便地構造List, Map 及使用閉包方便地遍歷這些容器,可使用元類方便地訪問Java類的私有成員。Groovy 是與 Java 系統集成的好夥伴。java

接口測試用例遵循「條件-結果檢測」模式,即:給定一系列條件,調用接口返回結果,並對結果進行校驗。若是結果與條件有關聯,那麼結果檢測須要將條件與結果關聯起來。接口測試用例必須是自動化可重複執行的。spring

對於訂單搜索來講,給定搜索條件 (CA=a, CB=b, CC=c, ......) ,調用訂單搜索接口返回的訂單列表中,應該校驗每個訂單知足 (RA=a, RB=b, RC=c, ......)。編程

出於對企業源代碼的保護,這裏業務敏感的信息和代碼會略去。 僅保留與 groovy 與 可配置化相關的內容,不影響讀者理解其中的技巧。

api

思路及實現

思路

這裏的關鍵點在於: 如何將訂單搜索條件與搜索結果的關聯作成可配置化的。 若是須要添加測試用例,只要增長新的配置項,而無需更改測試代碼。安全

訂單搜索條件,是設置 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 中含有訂單搜索的各類條件字段。app

@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 來表達聯合搜索條件的測試用例 (也能夠包裝爲一個含有 List 實例的 CompositeSearchTestCase 對象),貫穿整個條件設置及結果檢測。 單元測試

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 的使用有了更深刻的瞭解呢?

相關文章
相關標籤/搜索