接口测试 分享根据约束文件,生成测试用例的做法

cheunghr · 2021年03月04日 · 最后由 吴大熊 回复于 2023年05月22日 · 3596 次阅读

1、简介

​ 通过制定约束文件,通过等价类、边界值、正交法等方法,为单接口自动生成符合内部接口自动化平台导入规范的测试用例。

2、约束文件

2.1、约束文件示例

{
    "property": {
        "projectId": 4,
        "moduleId": 5,
        "url": "/user/register",
        "method": "post",
        "desc": "用户注册接口",
        "doc": "",
        "headers": "",
        "schemaType": "data",
        "validEquivalenceClass": {
            "caseLevel": "",
            "asserts": [
                {
                    "order": 1,
                    "desc": "http状态码需等于200",
                    "type": "code",
                    "operator": "=",
                    "expect": 200
                },
                {
                    "order": 2,
                    "desc": "接口状态码需等于200",
                    "type": "json",
                    "expression": "$..code",
                    "operator": "=",
                    "expect": 200
                }
            ]
        },
        "invalidEquivalenceClass": {
            "caseLevel": "",
            "asserts": [
                {
                    "order": 1,
                    "desc": "http状态码需等于200",
                    "type": "code",
                    "operator": "=",
                    "expect": 200
                },
                {
                    "order": 2,
                    "desc": "接口状态码不等于200",
                    "type": "json",
                    "expression": "$..code",
                    "operator": "!=",
                    "expect": 200
                }
            ]
        }
    },
    "schema": [
        {
            "name": "username",
            "desc": "用户名",
            "type": "NotInDb",
            "globalConfig": {
                "allowNull": false,
                "allowRepeat": false
            },
            "privateConfig": {
                "dbId": 1,
                "sql": "select username from t_user",
                "elementType": "String"
            }
        },
        {
            "name": "password",
            "desc": "密码",
            "type": "String",
            "globalConfig": {
                "allowNull": false,
                "allowRepeat": true
            },
            "privateConfig": {
                "allowIllegal": true,
                "allowEmpty": false,
                "minLen": 1,
                "maxLen": 10
            }
        },
        {
            "name": "realName",
            "desc": "真实姓名",
            "type": "String",
            "globalConfig": {
                "allowNull": true,
                "allowRepeat": true
            },
            "privateConfig": {
                "allowIllegal": false,
                "allowEmpty": true,
                "minLen": 1,
                "maxLen": 10
            }
        },
        {
            "name": "sex",
            "desc": "性别",
            "type": "InArray",
            "globalConfig": {
                "allowNull": false,
                "allowRepeat": true
            },
            "privateConfig": {
                "value": [0, 1],
                "elementType": "Integer"
            }
        }
    ]
}

2.2、属性释义

  • property:用例基础属性对象
    • projectId:项目编号(内部接口平台中的项目编号)
    • moduleId:模块编号(内部接口平台中的模块编号)
    • url:接口地址
    • method:请求方式(枚举值:get、post、patch、put、delete)
    • desc:接口描述(生成的用例名称会以该属性作为前缀)
    • doc:接口文档地址
    • headers:请求 headers(json 对象字符串)
    • schemaType:约束类型(枚举值:data、json、params,根据该字段确定约束生成的类型
    • validEquivalenceClass:有效等价类专有属性
    • caseLevel:用例等级(枚举值:高、中、低)
    • asserts:断言列表
      • order:断言排序
      • desc:断言描述
      • type:断言类型(枚举值:json、html、code)
      • operator:比较符(枚举值:=、<、>、<=、>=、in、!=、re)
      • expect:预期结果
    • invalidEquivalenceClass:无效等价类专有属性
    • caseLevel:用例等级(枚举值:高、中、低)
    • asserts:断言列表
      • order:断言排序
      • desc:断言描述
      • type:断言类型(枚举值:json、html、code)
      • operator:比较符(枚举值:=、<、>、<=、>=、in、!=、re)
      • expect:预期结果
  • schema:为接口中每个字段制定约束
    • name:接口字段名称
    • desc:字段描述
    • type:类型(枚举值:String、Number、InDb、NotInDb、Const、InArray、NotInArray;不同类型的 privateConfig 对象属性不一样
    • globalConfig
    • allowNull:是否允许为 null
    • allowRepeat:是否允许重复
    • privateConfig(type==String)
    • allowIllegal:是否允许非法字符
    • allowEmpty:是否允许空字符串
    • minLen:最小长度(含)
    • maxLen:最大长度(含)
    • privateConfig(type==Number)
    • min:最小值
    • max:最大值
    • privateConfig(type==InDb || type==NotInDb)
    • dbId:数据源编号(Alex 平台中的数据源编号)
    • sql:查询语句(仅支持查询单个字段)
    • elementType:查询结果返回值类型(枚举值:String、Number、Integer、Float、Double)
    • privateConfig(type==Const)
    • value:常量值
    • privateConfig(type==InArray || type==NotInArray)
    • value:数组
    • elementType:查询结果返回值类型(枚举值:String、Number、Integer、Float、Double)

2.3、生成结果示例

​ 根据上述约束文件生成的某个接口用例示例

{
    "projectId": 4,
    "moduleId": 5,
    "url": "/user/register",
    "method": "post",
    "desc": "无效等价类 用户注册接口 用户名<username>为null 密码<password>为null 真实姓名<realName>为null 性别<sex>为null",
    "level": "",
    "doc": "",
    "headers": "",
    "data": "{'realName':null,'password':null,'sex':null,'username':null}",
    "asserts": [{
        "expect": 200,
        "type": "code",
        "operator": "=",
        "order": 1,
        "desc": "http状态码需等于200"
    }, {
        "expect": 200,
        "expression": "$..code",
        "type": "json",
        "operator": "!=",
        "order": 2,
        "desc": "接口状态码不等于200"
    }]
}

3、核心类

  • Description

为生成的属性测试用例制定通用描述,最终应用在接口测试用例标题。如:

protected String desc4Null(String key, String desc) {
    return String.format("%s<%s>为null", desc, key);
}

protected String desc4Empty(String key, String desc) {
    return String.format("%s<%s>为空字符串", desc, key);
}
  • Filter

根据 schema[x].type,检查 schema[x].privateConfig 配置项数据合理性,如 type 为 String 时,privateConfig 将检查:

public void valid4String(Boolean allowIllegal, Boolean allowEmpty, Integer minLen, Integer maxLen) throws ValidException {
    ValidUtil.notNUll(allowIllegal, "type of String field valid error, allowIllegal should not be null");
    ValidUtil.notNUll(allowEmpty, "type of String field valid error, allowEmpty should not be null");
    ValidUtil.notNUll(minLen, "type of String field valid error, minLen should not be null");
    ValidUtil.notNUll(maxLen, "type of String field valid error, maxLen should not be null");
    ValidUtil.isGreaterThanOrEqualsZero(minLen, "type of String field valid error, minLen must greater than or equals 0");
    ValidUtil.isGreaterThanOrEqualsZero(maxLen, "type of String field valid error, maxLen must greater than or equals 0");
    if (minLen.compareTo(maxLen) > 0) {
        throw new ValidException("type of String field valid error, maxLen should greater than or equals minLen");
    }
}
  • Generator

为接口中单个属性,依据等价类、边界值等策略,生成属性测试用例,将结果"枚举化",如当 type 为 String 时,为字段生成的测试用例有:

/**
 * 生成字段类型为String的用例
 * @param key 字段名称
 * @param desc 字段描述
 * @param publicConfig 全局配置
 * @param allowIllegal 是否允许非法字符
 * @param allowEmpty 是否允许为空
 * @param minLen 最小长度
 * @param maxLen 最大长度
 * @return 字段用例
 */
private JSONArray genString(String key, String desc,
                       JSONObject publicConfig,
                       Boolean allowIllegal, Boolean allowEmpty, Integer minLen, Integer maxLen) {

    Boolean allowNull = publicConfig.getBoolean("allowNull");
    Boolean allowRepeat = publicConfig.getBoolean("allowRepeat");

    JSONArray result = new JSONArray();

    //1.null
    if (allowNull) {
        result.add(model(CaseType.VALID_EQUIVALENCE_CLASS, description.desc4Null(key, desc), null, key));
    } else {
        result.add(model(CaseType.INVALID_EQUIVALENCE_CLASS, description.desc4Null(key, desc), null, key));
    }

    //2.empty
    if (allowEmpty) {
        result.add(model(CaseType.VALID_EQUIVALENCE_CLASS, description.desc4Empty(key, desc), "", key));
    } else {
        result.add(model(CaseType.INVALID_EQUIVALENCE_CLASS, description.desc4Empty(key, desc), "", key));
    }

    //3.长度
    //A.长度范围内
    String randomLegalString = RandomUtil.randomLegalString(minLen, maxLen);
    result.add(model(CaseType.VALID_EQUIVALENCE_CLASS, description.desc4Length(key, desc, minLen, maxLen), randomLegalString, key));

    //B.恰好为最大长度
    String randomMax = RandomUtil.randomLegalStringByLength(maxLen);
    while (randomMax.equals(randomLegalString)) {
        randomMax = RandomUtil.randomLegalStringByLength(maxLen);
    }
    result.add(model(CaseType.VALID_EQUIVALENCE_CLASS, description.desc4EqualsMaxLength(key, desc, maxLen), randomMax, key));

    //C.恰好为最小长度
    String randomMin = RandomUtil.randomLegalStringByLength(minLen);
    while (randomMin.equals(randomLegalString) || randomMin.equals(randomMax)) {
        randomMin = RandomUtil.randomLegalStringByLength(minLen);
    }
    result.add(model(CaseType.VALID_EQUIVALENCE_CLASS, description.desc4EqualsMinLength(key, desc, minLen), randomMin, key));

    //D.恰好为最大长度+1
    result.add(model(CaseType.INVALID_EQUIVALENCE_CLASS, description.desc4GreaterLength(key, desc, maxLen, 1), RandomUtil.randomLegalStringByLength(maxLen + 1), key));

    //E.恰好为最小长度-1
    result.add(model(CaseType.INVALID_EQUIVALENCE_CLASS, description.desc4LessLength(key, desc, minLen, 1), RandomUtil.randomLegalStringByLength(minLen - 1), key));

    //4.非法字符
    if (allowIllegal) {
        result.add(model(CaseType.VALID_EQUIVALENCE_CLASS, description.desc4IllegalLength(key, desc, minLen, maxLen), RandomUtil.randomIllegalString(minLen, maxLen), key));
    } else {
        result.add(model(CaseType.INVALID_EQUIVALENCE_CLASS, description.desc4IllegalLength(key, desc, minLen, maxLen), RandomUtil.randomIllegalString(minLen, maxLen), key));
    }

    //5.重复
    if (allowRepeat) {
        result.add(model(CaseType.VALID_EQUIVALENCE_CLASS, description.desc4LengthRepeat(key, desc, minLen, maxLen), randomLegalString, key));
    } else {
        result.add(model(CaseType.INVALID_EQUIVALENCE_CLASS, description.desc4LengthRepeat(key, desc, minLen, maxLen), randomLegalString, key));
    }

    return result;
}

生成结果如下,共计 9 条:

[{
    "type": "invalidEquivalenceClass",
    "key": "password",
    "desc": "密码<password>为null"
}, {
    "type": "invalidEquivalenceClass",
    "value": "",
    "key": "password",
    "desc": "密码<password>为空字符串"
}, {
    "type": "validEquivalenceClass",
    "value": "ngw031",
    "key": "password",
    "desc": "密码<password>长度在区间[1,10]内"
}, {
    "type": "validEquivalenceClass",
    "value": "1kriino6qb",
    "key": "password",
    "desc": "密码<password>长度恰好为最大长度<10>"
}, {
    "type": "validEquivalenceClass",
    "value": "e",
    "key": "password",
    "desc": "密码<password>长度恰好为最小长度<1>"
}, {
    "type": "invalidEquivalenceClass",
    "value": "s2r9s39r96v",
    "key": "password",
    "desc": "密码<password>长度为最大长度<10>+1"
}, {
    "type": "invalidEquivalenceClass",
    "value": "",
    "key": "password",
    "desc": "密码<password>长度为最小长度<1>-1"
}, {
    "type": "validEquivalenceClass",
    "value": ",%)",
    "key": "password",
    "desc": "密码<password>非法字符长度在区间[1,10]内"
}, {
    "type": "validEquivalenceClass",
    "value": "ngw031",
    "key": "password",
    "desc": "密码<password>长度在区间[1,10]内-重复"
}],
  • Rule

根据Generator生成的属性测试用例,根据正交法、笛卡尔积组合成接口测试用例,生成的某条用例如下

[{
    "type": "validEquivalenceClass",
    "value": "21e9jzpaghq63",
    "key": "username",
    "desc": "用户名<username>值不存在于表内"
}, {
    "type": "validEquivalenceClass",
    "value": "x",
    "key": "password",
    "desc": "密码<password>长度恰好为最小长度<1>"
}, {
    "type": "validEquivalenceClass",
    "value": "n7aeqeid1s",
    "key": "realName",
    "desc": "真实姓名<realName>长度恰好为最大长度<10>"
}, {
    "type": "validEquivalenceClass",
    "value": 0,
    "key": "sex",
    "desc": "性别<sex>值存在于数组内-重复"
}],
  • Main

解析 Rule 生成的属性测试用例集合,根据自定义测试用例模版生成符合预期的接口测试用例

{
  "projectId": 4,
  "moduleId": 5,
  "url": "/user/register",
  "method": "post",
  "desc": "无效等价类 用户注册接口 用户名<username>为null 密码<password>为null 真实姓名<realName>为null 性别<sex>为null",
  "level": "",
  "doc": "",
  "headers": "",
  "data": "{'realName':null,'password':null,'sex':null,'username':null}",
  "asserts": [{
      "expect": 200,
      "type": "code",
      "operator": "=",
      "order": 1,
      "desc": "http状态码需等于200"
  }, {
      "expect": 200,
      "expression": "$..code",
      "type": "json",
      "operator": "!=",
      "order": 2,
      "desc": "接口状态码不等于200"
  }]
}

4、生成过程

  1. 读取约束文件的 schema 数组,获取每个约束对象(即接口中的某个属性,如:username);
  2. 根据约束对象类型(String、Number、InDb、NotInDb、Const、InArray、NotInArray),先对约束属性进行校验(通过 Filter),然后生成属性用例;
  3. Rule 类依据正交法、笛卡尔积生成组合测试用例;
  4. Main 类读取测试用例模版,解析 Rule 类生成结果,生成最终的测试用例。
共收到 11 条回复 时间 点赞

支持,可以连上 swagger,自动加上字段判断,一键式生成用例,就是字段太多的时通过笛卡尔生成的测试用例量过大

redbiscuit 回复

是有接入 swagger 的想法。另外还支持正交法,用例就没那么多啦

开源吗?

思路有点是不是类似 jsonchema

MBF 回复

嗯 类似

cheunghr 回复

求个演示账号

Colin 回复

readme 上有写,youke/admini0

10楼 已删除
cheunghr 回复

你好,密码不对,登不了,然后注册了一个,只能看到首页

清明雨上 回复

我試了是可以使用 youke/admini0 登录的

你好 想问下如果字段参数有嵌套的时候,应该怎么进行配置约束文件的?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册