接口测试 Python 实现正交实验法设计测试用例,自动生成测试集

Joo · 2018年07月20日 · 最后由 space 回复于 2019年07月07日 · 3761 次阅读

1.简介

正交试验法是研究多因素、多水平的一种试验法,它是利用正交表来对试验进行设计,通过少数的试验替代全面试验,根据正交表的正交性从全面试验中挑选适量的、有代表性的点进行试验,这些有代表性的点具备了 “均匀分散,整齐可比” 的特点。

正交实验法设计测试用例,基本步骤如下:

  1. 提取测试需求功能说明,确定因素数和水平数
  2. 根据因素数和水平数确定 n 值
  3. 选择合适的正交表
  4. 根据正交表把变量的值映射到表中,设计测试用例数据集

项目 GitHub 地址:https://github.com/lovesoo/OrthogonalArrayTest

2.Python 实现逻辑详解

参考如上步骤,使用 Python 实现了使用正交表自动设计裁剪测试用例的完整流程。支持 Python 版本为 2.7, 3.7。

  1. 初始化正交表,解析构造为可用的正交表对象数组(数据来源:http://support.sas.com/techsup/technote/ts723_Designs.txt

  2. 分别计算 m(水平数),k(因素数目),n(实验次数)值

    m=max(m1,m2,m3,…)

    k=(k1+k2+k3+…)

    n=k1*(m1-1)+k2*(m2-1)+…kx*x-1)+1

  3. 查找匹配正交表,先查询是否有完全匹配的正交表数据,否则简单处理,只返回满足>=m,n,k 条件的 n 最小数据,暂未做复杂的数组包含校验及筛选逻辑(后续待优化)

  4. 使用查找到的正交表数据,裁剪生成测试集。支持两种用例裁剪模式,取值 0,1

    • 0 宽松模式,只裁剪重复测试集
    • 1 严格模式,除了裁剪重复测试集外,还裁剪含 None 测试集 (num 为允许 None 测试集最大数目)

完整代码如下:

# encoding: utf-8

from itertools import groupby
from collections import OrderedDict
import os


def dataSplit(data):
    ds = []
    mb = [sum([k for m, k in data['mk'] if m <= 10]), sum([k for m, k in data['mk'] if m > 10])]
    for i in data['data']:
        if mb[1] == 0:
            ds.append([int(d) for d in i])
        elif mb[0] == 0:
            ds.append([int(i[n * 2:(n + 1) * 2]) for n in range(mb[1])])
        else:
            part_1 = [int(j) for j in i[:mb[0]]]
            part_2 = [int(i[mb[0]:][n * 2:(n + 1) * 2]) for n in range(mb[1])]
            ds.append(part_1 + part_2)
    return ds


class OAT(object):
    def __init__(self, OAFile=os.path.split(os.path.realpath(__file__))[0] + '/data/ts723_Designs.txt'):
        """
        初始化解析构造正交表对象,数据来源:http://support.sas.com/techsup/technote/ts723_Designs.txt
        """
        self.data = {}

        # 解析正交表文件数据
        with open(OAFile, ) as f:
            # 定义临时变量
            key = ''
            value = []
            pos = 0

            for i in f:
                i = i.strip()
                if 'n=' in i:
                    if key and value:
                        self.data[key] = dict(pos=pos,
                                              n=int(key.split('n=')[1].strip()),
                                              mk=[[int(mk.split('^')[0]), int(mk.split('^')[1])] for mk in key.split('n=')[0].strip().split(' ')],
                                              data=value)
                    key = ' '.join([k for k in i.split(' ') if k])
                    value = []
                    pos += 1
                elif i:
                    value.append(i)

            self.data[key] = dict(pos=pos,
                                  n=int(key.split('n=')[1].strip()),
                                  mk=[[int(mk.split('^')[0]), int(mk.split('^')[1])]for mk in key.split('n=')[0].strip().split(' ')],
                                  data=value)
        self.data = sorted(self.data.items(), key=lambda i: i[1]['pos'])

    @staticmethod
    def get(self, mk):
        """
        传入参数:mk列表,如[(2,3)],[(5,5),(2,1)]

        1. 计算m,n,k
        m=max(m1,m2,m3,…)
        k=(k1+k2+k3+…)
        n=k1*(m1-1)+k2*(m2-1)+…kx*x-1)+1

       2. 查询正交表
        这里简单处理,只返回满足>=m,n,k条件的n最小数据,未做复杂的数组包含校验
        """
        mk = sorted(mk, key=lambda i: i[0])

        m = max([i[0] for i in mk])
        k = sum([i[1] for i in mk])
        n = sum([i[1] * (i[0] - 1) for i in mk]) + 1
        query_key = ' '.join(['^'.join([str(j) for j in i]) for i in mk])

        for data in self.data:
            # 先查询是否有完全匹配的正交表数据
            if query_key in data[0]:
                return dataSplit(data[1])
            # 否则返回满足>=m,n,k条件的n最小数据
            elif data[1]['n'] >= n and data[1]['mk'][0][0] >= m and data[1]['mk'][0][1] >= k:
                return dataSplit(data[1])
        # 无结果
        return None

    def genSets(self, params, mode=0, num=1):
        """
        传入测试参数OrderedDict,调用正交表生成测试集
        mode:用例裁剪模式,取值0,1
            0 宽松模式,只裁剪重复测试集
            1 严格模式,除裁剪重复测试集外,还裁剪含None测试集(num为允许None测试集最大数目)
        """
        sets = []
        mk = [(k, len(list(v)))for k, v in groupby(params.items(), key=lambda x:len(x[1]))]
        data = OAT.get(self, mk)
        for d in data:
            # 根据正则表结果生成测试集
            q = OrderedDict()
            for index, (k, v) in zip(d, params.items()):
                try:
                    q[k] = v[index]
                except IndexError:
                    # 参数取值超出范围时,取None
                    q[k] = None
            if q not in sets:
                if mode == 0:
                    sets.append(q)
                elif mode == 1 and (len(list(filter(lambda v: v is None, q.values())))) <= num:
                    # 测试集裁剪,去除重复及含None测试集
                    sets.append(q)
        return sets

3.示例 demo

我们复用之前写过的豆瓣电影搜索接口,针对这个接口编写了一个正交表生成裁剪用例 Demo (接口文档地址:https://developers.douban.com/wiki/?title=movie_v2#search)

  1. 首先我们看到,这个接口支持四个参数 q,tag,start,count

  2. 针对每个参数分别设计了一些测试集,如果使用原来的组合方式生成测试用例条数为每个参数笛卡尔乘积(8 * 6 * 3 * 3),而使用正交表生成的测试用例数目为 7,大大削减了无效重复的测试条目,提升测试效率

执行结果如下(有一条用例出错是因为,根据指定条件查询结果为空,所以结果校验失败):

脚本如下:

# encoding: utf-8

from OAT import *
import json
import requests
from functools import partial
from nose.tools import *

"""
pip install requests
pip install nose
"""


class check_response():
    @staticmethod
    def check_result(response, params, expectNum=None):
        # 由于搜索结果存在模糊匹配的情况,这里简单处理只校验第一个返回结果的正确性
        if expectNum is not None:
            # 期望结果数目不为None时,只判断返回结果数目
            eq_(expectNum, len(response['subjects']), '{0}!={1}'.format(expectNum, len(response['subjects'])))
        else:
            if not response['subjects']:
                # 结果为空,直接返回失败
                assert False
            else:
                # 结果不为空,校验第一个结果
                subject = response['subjects'][0]
                # 先校验搜索条件tag
                if params.get('tag'):
                    for word in params['tag'].split(','):
                        genres = subject['genres']
                        ok_(word in genres, 'Check {0} failed!'.format(word))

                # 再校验搜索条件q
                elif params.get('q'):
                    # 依次判断片名,导演或演员中是否含有搜索词,任意一个含有则返回成功
                    for word in params['q'].split(','):
                        title = [subject['title']]
                        casts = [i['name'] for i in subject['casts']]
                        directors = [i['name'] for i in subject['directors']]
                        total = title + casts + directors
                        ok_(any(word.lower() in i.lower() for i in total),
                            'Check {0} failed!'.format(word))


class test_douban(object):
    """
    豆瓣搜索接口测试demo,文档地址 https://developers.douban.com/wiki/?title=movie_v2#search
    """

    def search(self, params, expectNum=None):
        url = 'https://api.douban.com/v2/movie/search'
        r = requests.get(url, params=params)
        print ('Search Params:\n', json.dumps(params, ensure_ascii=False))
        print ('Search Response:\n', json.dumps(r.json(), ensure_ascii=False, indent=4))
        code = r.json().get('code', 0)
        if code > 0:
            assert False, 'Invoke Error.Code:\t{0}'.format(code)
        else:
            # 校验搜索结果是否与搜索词匹配
            check_response.check_result(r.json(), params, expectNum)

    def test_q(self):
        # 校验搜索条件
        qs = [u'白夜追凶', u'大话西游', u'周星驰', u'张艺谋', u'周星驰,吴孟达', u'张艺谋,巩俐', u'周星驰,西游', u'白夜追凶,潘粤明']
        tags = [u'科幻', u'喜剧', u'动作', u'犯罪', u'科幻,喜剧', u'动作,犯罪']
        starts = [0, 10, 20]
        counts = [20, 10, 5]

        # 生成原始测试数据 (有序数组)
        cases = OrderedDict([('q', qs), ('tag', tags), ('start', starts), ('count', counts)])

        # 使用正交表裁剪生成测试集
        cases = OAT().genSets(cases, mode=1, num=0)

        # 执行测试用例
        for case in cases:
            f = partial(self.search, case)
            f.description = json.dumps(case, ensure_ascii=False)
            yield (f,)

4.后续计划

  1. 判定表查询逻辑优化
  2. 测试用例集裁剪优化

5.参考文档

  1. 测试用例设计 - 正交实验法详解: https://wenku.baidu.com/view/a54724156edb6f1aff001f79.html
  2. 用正交实验法设计测试用例:http://blog.csdn.net/fangnannanf/article/details/52813498
  3. Dr. Genichi Taguchi 设计的正交表:http://www.york.ac.uk/depts/maths/tables/orthogonal.htm
  4. Technical Support com:http://support.sas.com/techsup/technote/ts723_Designs.txt
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 11 条回复 时间 点赞

首先为楼主点赞
曾经我也想过这个问题: 如何通过程序生成用例;
蛋考虑到好多工程师,甚至是所谓资深测试,都不见的在理论上搞的清楚这些真正有价值的用例设计方法,
只在各种编码相关的领域 ta 儿哄,所以果断放弃;

当然我也不明白。。。
基础学科就是差,没办法

反正都让代码帮你做事,多做点又何妨。通常我是跑笛卡尔全集的用例

正交实验法有个问题,就是如何控制用例组合的爆炸?比如一个字段可能有 12 个取值,一个用例有 12 个字段,组合出来的矩阵太多了,另外一个有难度的事期望结果如何能智能的对号入座

simple 回复

我的方法是使用单一变量法,可简化大量用例。

simple 回复

对的 ,组合多个用例,期望值我现在也无法对比,现在是没有对比期望值,最后手动在 excel 中填...

我们用 pairwise 组合覆盖至少 2 个字段的正交矩阵,这样的覆盖率已经超出手工设计 case 覆盖率很多,另外期望结果我们用工具自动生成的,提供对应的模版来对号入座(我们业务比较特殊,只需要校验 error code 就行)

匿名 #7 · 2019年05月17日

代码有点问题呐,1112 的正交表,水平值为 0-10,按你得这个代码取值能取出 81。就是这一句` 0 2 4 6 810 1 3 5 7 9 2
`,应该是 8 和 10,而不是 81 和 0。还有一个明显的错误,静态方法怎么又用调用 self?

11^12     n=121
 0 0 0 0 0 0 0 0 0 0 0 0
 0 1 2 3 4 5 6 7 8 910 1
 0 2 4 6 810 1 3 5 7 9 2
 0 3 6 9 1 4 710 2 5 8 3
 0 4 8 1 5 9 2 610 3 7 4
 0 510 4 9 3 8 2 7 1 6 5
 0 6 1 7 2 8 3 9 410 5 6
 0 7 310 6 2 9 5 1 8 4 7
 0 8 5 210 7 4 1 9 6 3 8
 0 9 7 5 3 110 8 6 4 2 9
 010 9 8 7 6 5 4 3 2 110
 1 010 9 8 7 6 5 4 3 210
 1 1 1 1 1 1 1 1 1 1 1 0
 1 2 3 4 5 6 7 8 910 0 1
 1 3 5 7 9 0 2 4 6 810 2
 1 4 710 2 5 8 0 3 6 9 3
 1 5 9 2 610 3 7 0 4 8 4
 1 6 0 510 4 9 3 8 2 7 5
 1 7 2 8 3 9 410 5 0 6 6
 1 8 4 0 7 310 6 2 9 5 7
 1 9 6 3 0 8 5 210 7 4 8
 110 8 6 4 2 0 9 7 5 3 9
 2 0 9 7 5 3 110 8 6 4 9
 2 1 010 9 8 7 6 5 4 310
 2 2 2 2 2 2 2 2 2 2 2 0
 2 3 4 5 6 7 8 910 0 1 1
 2 4 6 810 1 3 5 7 9 0 2
 2 5 8 0 3 6 9 1 4 710 3
 2 610 3 7 0 4 8 1 5 9 4
 2 7 1 6 0 510 4 9 3 8 5
 2 8 3 9 410 5 0 6 1 7 6
 2 9 5 1 8 4 0 7 310 6 7
 210 7 4 1 9 6 3 0 8 5 8
 3 0 8 5 210 7 4 1 9 6 8
 3 110 8 6 4 2 0 9 7 5 9
 3 2 1 010 9 8 7 6 5 410
 3 3 3 3 3 3 3 3 3 3 3 0
 3 4 5 6 7 8 910 0 1 2 1
 3 5 7 9 0 2 4 6 810 1 2
 3 6 9 1 4 710 2 5 8 0 3
 3 7 0 4 8 1 5 9 2 610 4
 3 8 2 7 1 6 0 510 4 9 5
 3 9 410 5 0 6 1 7 2 8 6
 310 6 2 9 5 1 8 4 0 7 7
 4 0 7 310 6 2 9 5 1 8 7
 4 1 9 6 3 0 8 5 210 7 8
 4 2 0 9 7 5 3 110 8 6 9
 4 3 2 1 010 9 8 7 6 510
 4 4 4 4 4 4 4 4 4 4 4 0
 4 5 6 7 8 910 0 1 2 3 1
 4 6 810 1 3 5 7 9 0 2 2
 4 710 2 5 8 0 3 6 9 1 3
 4 8 1 5 9 2 610 3 7 0 4
 4 9 3 8 2 7 1 6 0 510 5
 410 5 0 6 1 7 2 8 3 9 6
 5 0 6 1 7 2 8 3 9 410 6
 5 1 8 4 0 7 310 6 2 9 7
 5 210 7 4 1 9 6 3 0 8 8
 5 3 110 8 6 4 2 0 9 7 9
 5 4 3 2 1 010 9 8 7 610
 5 5 5 5 5 5 5 5 5 5 5 0
 5 6 7 8 910 0 1 2 3 4 1
 5 7 9 0 2 4 6 810 1 3 2
 5 8 0 3 6 9 1 4 710 2 3
 5 9 2 610 3 7 0 4 8 1 4
 510 4 9 3 8 2 7 1 6 0 5
 6 0 510 4 9 3 8 2 7 1 5
 6 1 7 2 8 3 9 410 5 0 6
 6 2 9 5 1 8 4 0 7 310 7
 6 3 0 8 5 210 7 4 1 9 8
 6 4 2 0 9 7 5 3 110 8 9
 6 5 4 3 2 1 010 9 8 710
 6 6 6 6 6 6 6 6 6 6 6 0
 6 7 8 910 0 1 2 3 4 5 1
 6 810 1 3 5 7 9 0 2 4 2
 6 9 1 4 710 2 5 8 0 3 3
 610 3 7 0 4 8 1 5 9 2 4
 7 0 4 8 1 5 9 2 610 3 4
 7 1 6 0 510 4 9 3 8 2 5
 7 2 8 3 9 410 5 0 6 1 6
 7 310 6 2 9 5 1 8 4 0 7
 7 4 1 9 6 3 0 8 5 210 8
 7 5 3 110 8 6 4 2 0 9 9
 7 6 5 4 3 2 1 010 9 810
 7 7 7 7 7 7 7 7 7 7 7 0
 7 8 910 0 1 2 3 4 5 6 1
 7 9 0 2 4 6 810 1 3 5 2
 710 2 5 8 0 3 6 9 1 4 3
 8 0 3 6 9 1 4 710 2 5 3
 8 1 5 9 2 610 3 7 0 4 4
 8 2 7 1 6 0 510 4 9 3 5
 8 3 9 410 5 0 6 1 7 2 6
 8 4 0 7 310 6 2 9 5 1 7
 8 5 210 7 4 1 9 6 3 0 8
 8 6 4 2 0 9 7 5 3 110 9
 8 7 6 5 4 3 2 1 010 910
 8 8 8 8 8 8 8 8 8 8 8 0
 8 910 0 1 2 3 4 5 6 7 1
 810 1 3 5 7 9 0 2 4 6 2
 9 0 2 4 6 810 1 3 5 7 2
 9 1 4 710 2 5 8 0 3 6 3
 9 2 610 3 7 0 4 8 1 5 4
 9 3 8 2 7 1 6 0 510 4 5
 9 410 5 0 6 1 7 2 8 3 6
 9 5 1 8 4 0 7 310 6 2 7
 9 6 3 0 8 5 210 7 4 1 8
 9 7 5 3 110 8 6 4 2 0 9
 9 8 7 6 5 4 3 2 1 01010
 9 9 9 9 9 9 9 9 9 9 9 0
 910 0 1 2 3 4 5 6 7 8 1
10 0 1 2 3 4 5 6 7 8 9 1
10 1 3 5 7 9 0 2 4 6 8 2
10 2 5 8 0 3 6 9 1 4 7 3
10 3 7 0 4 8 1 5 9 2 6 4
10 4 9 3 8 2 7 1 6 0 5 5
10 5 0 6 1 7 2 8 3 9 4 6
10 6 2 9 5 1 8 4 0 7 3 7
10 7 4 1 9 6 3 0 8 5 2 8
10 8 6 4 2 0 9 7 5 3 1 9
10 9 8 7 6 5 4 3 2 1 010
1010101010101010101010 0
Joo #8 · 2019年05月18日 Author

感谢,我看下问题,修改下~ 👍 👍 👍

请问这个去除含 none 的测试集会不会导致少了很多实际需要用到的用例呢?我看到有些帖子是把超出范围的值用范围内的值填充上去的

Joo #10 · 2019年07月01日 Author
space 回复

None 用例的话,我目前是通过一个参数控制的;如果填一个合理值也可以,但是应该会导致用例重复吧

Joo 回复

嗯嗯

需要 登录 後方可回應,如果你還沒有帳號按這裡 注册