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

相关文章
相关标签/搜索