你在开发unittest/pytest/nose+requests+htmltestrunner/allure框架时还在被数据驱动、参数化、关联接口这些概念误导吗?

如果觉得文章对您提升有所帮助,请帮忙点赞、收藏、转发、留言提升下热度让更多人看到,关注随意,仅依赖平台的推广流量太有限了,谢谢大家!

想必大家在讨论接口自动化框架解决方案的时候都会提到数据驱动、参数化、关联接口这些名词,这些名词的来源均来自于jmeter的功能设计,由于jmeter是先于接口自动化框架的接口测试解决方案,所以这些概念在后来设计开发接口自动化框架的时候都被沿用了,并且发展成了行话。而这每个名称其实对应的是一种功能设计描述。在接口自动化测试这项工作上,jmeter和接口自动化框架是不同的技术选型,而针对各自的解决方案,无论是设计或概念都是独立的,设计可以参考但不能照搬,现如今那些基于python或java的接口自动化框架在照搬jmeter的数据驱动、参数化、关联接口设计后,与其说是接口自动化的代码解决方案,不如说是代码化的jmeter,既然通过纯代码的方式去解决就要优化数据驱动、参数化依赖外部文件存储数据这种半自动化设计。不然有什么技术上的优化呢,通过代码去解决的意义在哪呢?之所以要提出不同设计方案,是为了去掉外部文件存储数据的设计达到降低维护成本的目的,当接入框架的应用数量有10+时,外部文件的维护是一个甩不掉的累赘

下面我们通过表格的形式去对比接口自动化测试对应本质工作在jmeter和接口自动化框架的设计/概念:

本质工作设计/概念
jmeterpython/java
接口请求相关静态数据处理方案数据驱动copy库开发参数设置工具
接口入参参数值动态处理以便模拟更真实的测试场景参数化动态值
处理要测试接口的入参参数值来源于一个或多个其它接口的场景关联接口接口上下文

从对比可知,做接口自动化测试的方案时,无论jmeter还是代码方式,它们有共同要完成的功能,而且功能的设计是不一样的,代码解决方案就是要纯代码方式实现全自动化的效果,如果还照搬jmeter的功能设计是不可取的也是无意义的开发工作

下面我们讨论基于python的静态数据处理、动态值、接口上下文的全自动化效果的解决方案。

一、接口请求相关静态数据处理方案,对应数据驱动的设计

python完全可以以纯代码的方式解决静态数据的处理,基于copy.deepcopy开发的参数设置工具是更好的设计方案,这一点在之前的文章中已经讲过,可以看我之前的文章:unittest/pytest/nose+requests+htmltestrunner/allure真的存在数据驱动的概念吗?

二、接口入参参数值动态处理以便模拟更真实的测试场景,对应参数化

这一点在python里可以轻而易举的实现,甚至谈不上什么设计,就是以工具的形式提供一个可以生产随机数据结构的模块,比如字符串、时间、电话、地区、身份证、性别等等。接口参数赋值的时候直接拿来导入引用即可,根据自己的应用/项目的业务实时开发、维护。这里不建议使用faker,faker生产的动态值,很多时候并不适用,根据应用/项目的业务定制是更好的选择。通常这类动态值功能在你换工作之后,可能并不适用于新工作的业务,是的,删除掉重新开发即可。但是有一些动态值是无论新工作的应用/项目是什么行业或业务,都会用到的,这里我也分享给大家

以下是我的整理:

import datetime
import random
import string
import time

'''数值'''


def random_int_num(num):
    '''指定1~num数值范围内的整数'''
    random_num = random.randint(1, num)
    return random_num


def random_int_x_num(x, num):
    '''指定x~num数值范围内的整数'''
    random_num = random.randint(x, num)
    return random_num


def random_len_num(lenth):
    '''指定长度的整数'''
    num_list = [random.choice(string.digits) for i in range(lenth)]
    random_num = int(''.join(num_list))
    return random_num


def float_two(start, end):
    '''start~end范围内的小数,保留小数点后2位'''
    value = random.uniform(start, end)
    # 保留两位小数
    return round(value, 2)


'''字符串'''


def random_len_str(lenth):
    '''指定长度随机字符串;使用场景:同时需要修改多个参数,而需要修改的参数中的值需要通过该方法获得,一般结合合ParameterSetting.customize_value()使用'''
    str_list = [random.choice(string.digits + string.ascii_letters) for i in range(lenth)]
    random_str = ''.join(str_list)
    return random_str


def randomStrOrNumNotInlist(sample, lenth, strType=True):
    '''随机一个不在sample列表里的str或num,用lenth指定长度,strType:True是str,False为num'''
    if strType:
        while True:
            li = [random.choice(string.digits + string.ascii_letters) for i in range(lenth)]
            random_str = ''.join(li)
            if random_str not in sample:
                break
        return random_str
    else:
        while True:
            li = [random.choice(string.digits) for i in range(lenth)]
            random_str = ''.join(li)
            if random_str not in sample:
                break
        return random_str


'''布尔型'''


def genBoolValue():
    '''布尔值,随机返回0或1'''
    bool_v = random.choice((0, 1))
    return bool_v


'''特定类型'''


def get_addr():
    provice_list = ["湖北省", "湖南省", "江西省", "山东省", "海南省", "浙江省", "河北省", "黑龙江省", "吉林省", "辽宁省",
                    "河南省", "陕西省", "山西省", "广东省", "广西省", "云南省"]
    provice = random.choice(provice_list)
    return provice


def get_city():
    citylist = ["唐山市", "哈尔滨市", "长春市", "四平市", "沈阳市", "大理市", "丽江市", "襄阳市", "黄冈市", "鄂州市",
                "黄石市", "武汉市", "咸宁市", "宜昌市", "南昌市", "恩施市", "长沙市", "衡阳市"]
    city = random.choice(citylist)
    return city

def genName():
    a1 = ['张', '王', '赵', '秦', '孙', '李', '刘', '公孙', '司马', '宫', '慕容']
    a2 = ['德', '美', '群', '飞', '金', '智']
    a3 = ['龙', '华', '奇', '瑶', '尧', '浩', '冰', '兵', '伟', '涛']
    name = f'{random.choice(a1)}{random.choice(a2)}{random.choice(a3)}'
    return name


def sex(sex_type: int = 1):
    if sex_type == 1:
        sex_list = ['男', '女', '其它']
        sex = random.choice(sex_list)
    elif sex_type == 2:
        sex_list = ['femal', 'male', 'other']
        sex = random.choice(sex_list)
    elif sex_type == 3:
        sex_list = ['F', 'M']
        sex = random.choice(sex_list)
    elif sex_type == 4:
        sex_list = [0, 1]
        sex = random.choice(sex_list)
    else:
        print('未知性别类型编号...')
    return sex


# 随机手机号
def random_mobile():
    for k in range(10):
        prelist = ["130", "131", "132", "133", "134", "135", "136", "137", "138", "139",
                   "147", "150", "151", "152", "153", "155", "156", "157", "158", "159",
                   "186", "187", "188", "189"]
        phone = random.choice(prelist) + "".join(random.choice("0123456789") for i in range(8))
        return phone


# 身份证
class IdNumber(str):
    def __init__(self, id_number):
        super(IdNumber, self).__init__()
        self.id = id_number

    def get_check_digit(self):
        """通过身份证号获取校验码"""
        check_sum = 0
        for i in range(0, 17):
            check_sum += ((1 << (17 - i)) % 11) * int(self.id[i])
        check_digit = (12 - (check_sum % 11)) % 11
        return check_digit if check_digit < 10 else "X"

    @classmethod
    def generate_myid(cls):
        '''身份证号'''
        generate_ids = []
        # 随机生成一个区域码(6位数)
        area_code = "412826"
        # 限定出生日期范围(8位数)
        birth_day = "19610420"

        # 顺序码(2位数)
        for i in range(100):
            sort_no = f"{i:02d}"
            for j in [x for x in range(10) if x % 2 != 0]:
                sex = j
                prefix = f"{area_code}{birth_day}{sort_no}{sex}"
                valid_bit = str(cls(prefix).get_check_digit())
                generate_ids.append(f"{prefix}{valid_bit}")
        # return generate_ids
        return random.choice(generate_ids)


'''时间类型:格式化时间&时间戳'''


def customize_format_time(start_time: tuple = None, end_time: tuple = None, time_format: str = '%Y-%m-%d %H:%M:%S'):
    '''
    :param start_time:
    :param end_time:
    :param time_format:时间格式
    :return: 获取自定义时间范围内自定义格式的格式化时间
    '''
    # start_time = (1976, 1, 1, 0, 0, 0, 0, 0, 0)  # 设置开始日期时间元组(1976-01-01 00:00:00)
    # end_time = (2021, 12, 31, 23, 59, 59, 0, 0, 0)  # 设置结束日期时间元组(2022-12-31 23:59:59)
    if start_time is None and end_time is None:
        start = time.mktime((1976, 1, 1, 0, 0, 0, 0, 0, 0))
        end = time.mktime(time.localtime())
        t = random.randint(start, end)
        date_tuple = time.localtime(t)
        date = time.strftime(time_format, date_tuple)

    elif start_time is None and end_time is not None:
        start = time.mktime((1976, 1, 1, 0, 0, 0, 0, 0, 0))
        end = time.mktime(end_time)
        t = random.randint(start, end)
        date_tuple = time.localtime(t)
        date = time.strftime(time_format, date_tuple)

    elif start_time is not None and end_time is None:
        start = time.mktime(start_time)
        end = time.mktime(time.localtime())
        t = random.randint(start, end)
        date_tuple = time.localtime(t)
        date = time.strftime(time_format, date_tuple)

    else:
        start = time.mktime(start_time)
        end = time.mktime(end_time)
        t = random.randint(start, end)
        date_tuple = time.localtime(t)
        date = time.strftime(time_format, date_tuple)
    return date


def customize_timestamp(start_time: tuple = None, end_time: tuple = None, time_units: int = 1):
    '''
    :param start_time:
    :param end_time:
    :param time_units: 时间单位,1秒2毫秒3微秒
    :return: 自定义时间范围内的时间戳
    '''
    # start_time = (1976, 1, 1, 0, 0, 0, 0, 0, 0)  # 设置开始日期时间元组(1976-01-01 00:00:00)
    # end_time = (2021, 12, 31, 23, 59, 59, 0, 0, 0)  # 设置结束日期时间元组(2022-12-31 23:59:59)
    if start_time is None and end_time is None:
        start = time.mktime((1976, 1, 1, 0, 0, 0, 0, 0, 0))
        end = time.mktime(time.localtime())
        t = random.randint(start, end)
        if time_units == 1:
            date = round(t)
        elif time_units == 2:
            date = round(t * 1000)
        else:
            date = round(t * 1000000)

    elif start_time is None and end_time is not None:
        start = time.mktime((1976, 1, 1, 0, 0, 0, 0, 0, 0))
        end = time.mktime(end_time)
        t = random.randint(start, end)
        if time_units == 1:
            date = round(t)
        elif time_units == 2:
            date = round(t * 1000)
        else:
            date = round(t * 1000000)

    elif start_time is not None and end_time is None:
        start = time.mktime(start_time)
        end = time.mktime(time.localtime())
        t = random.randint(start, end)
        if time_units == 1:
            date = round(t)
        elif time_units == 2:
            date = round(t * 1000)
        else:
            date = round(t * 1000000)

    else:
        start = time.mktime(start_time)
        end = time.mktime(end_time)
        t = random.randint(start, end)
        if time_units == 1:
            date = round(t)
        elif time_units == 2:
            date = round(t * 1000)
        else:
            date = round(t * 1000000)
    return date


def current_timestrip():
    date = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    return date


# 当前时间毫秒时间戳
def current_millisecond_timestamp():
    datetimestamp = time.time()
    return int(round(datetimestamp * 1000))


# 当前时间微秒时间戳
def current_subtle_timestamp():
    datetimestamp = time.time()
    return int(round(datetimestamp * 1000000))


# 当前日期和时间,格式:2021-03-31 11:23:16
def current_datetime():
    datetime_current = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    return datetime_current

# 多列表取交集
def list_intersection(*args: list):
    new_list = set.intersection(*map(set, args))
    return new_list

实际测试代码引用动态值的效果,代码长什么样我也给大家演示一下:

class Bind(unittest.TestCase):
    def __init__(self, methodName):
        super().__init__(methodName=methodName)
        self.ch = configHttp.ConfigHttp()
        api = 'api_name'
        self.url = self.ch.set_url(self.ch.https, ReadConfig.x_config(expression='application_info["ali"]', name='host'), api)
        self.payload = {
            "extend": '{}',
            "seller_name": AliMember().seller_name,
            "open_id": random_len_str(24),
            "ouid": random_len_str(24),
            "mobile": AliMember().mobile,
            "mix_mobile": AliMember().mix_mobile,
            "type": '1',
            "taobao_nick": random_len_str(10),
            "omid": random_len_str(26)
        }

    @classmethod
    def setUpClass(cls):
        # InsecureRequestWarning
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
        # description set
        cls.function_description = 'function_description-'
        cls.case_description = '[用例描述]'

    @classmethod
    def tearDownClass(cls):
        logger = MyLog.get_log().get_logger()
        logger.info(f'All scenarios  of {__class__.__name__} have been tested')

    @mark(value='smoke')
    def test_01(self):
        print(f'{self.case_description}{self.function_description}场景1')
        am = AliMember()
        mobile = am.mobile
        mix_mobile = am.mix_mobile
        payload = ParameterSetting(self.payload).customize_value({'seller_name': AliMember().seller_name, 'mix_mobile': mix_mobile, 'mobile': mobile})
        traceId = get_traceId(self._testMethodName)
        headers = self.ch.set_headers(traceId=traceId)
        response = self.ch.post(self.url, headers, payload)
        self.assertEqual('SUC', response['register_code'])

    @mark(value=['smoke', 'p0'])
    def test_02(self):
        # 不同的会员,ouid和omid一定是都不相同的,不会存在ouid、omid有一个一样另一个不一样的情况,所以ouid、omid重复的这种场景,只需要写一个即可
        print(f'{self.case_description}{self.function_description}场景2')
        am = AliMember()
        mix_mobile = am.mix_mobile
        mobile = am.mobile
        ouid = random_len_str(24)
        payload = ParameterSetting(self.payload).customize_value({'ouid': ouid, 'mix_mobile': mix_mobile, 'mobile': mobile})
        self.ch.post(self.url, self.ch.set_headers(), payload)
        payload_two = ParameterSetting(payload).customize_value({'ouid': ouid, 'mix_mobile': mix_mobile, 'mobile': mobile, 'omid': random_len_str(26)})
        traceId = get_traceId(self._testMethodName)
        headers = self.ch.set_headers(traceId=traceId)
        response = self.ch.post(self.url, headers, payload_two)
        self.assertEqual('SUC', response['register_code'])

    @mark(value=['smoke', 'p1'])
    def test_03(self):
        print(f'{self.case_description}{self.function_description}场景3')
        am = AliMember()
        mix_mobile = am.mix_mobile
        mobile = am.mobile
        payload = ParameterSetting(self.payload).customize_value({'omid': random_len_str(24), 'mix_mobile': mix_mobile, 'mobile': mobile, 'ouid': random_len_str(24)})
        self.ch.post(self.url, self.ch.set_headers(), payload)
        payload_two = ParameterSetting(payload).customize_value({'omid': random_len_str(24), 'mix_mobile': mix_mobile, 'mobile': mobile, 'ouid': random_len_str(24)})
        traceId = get_traceId(self._testMethodName)
        headers = self.ch.set_headers(traceId=traceId)
        response = self.ch.post(self.url, headers, payload_two)
        self.assertEqual(response['register_code'], 'E03')

    def test_04(self):
        print(f'{self.case_description}{self.function_description}场景4')
        am = AliMember()
        mix_mobile = am.mix_mobile
        mobile = am.mobile
        payload = ParameterSetting(self.payload).customize_value({'omid': random_len_str(24), 'mobile': mobile, 'ouid': random_len_str(24), 'mix_mobile': mix_mobile})
        self.ch.post(self.url, self.ch.set_headers(), payload)
        payload_two = ParameterSetting(payload).customize_value({'omid': random_len_str(24), 'mobile': mobile, 'mix_mobile': mix_mobile, 'ouid': random_len_str(24)})
        traceId = get_traceId(self._testMethodName)
        headers = self.ch.set_headers(traceId=traceId)
        response = self.ch.post(self.url, headers, payload_two)
        self.assertEqual(response['register_code'], 'E03')

    def test_05(self):
        print(f'{self.case_description}{self.function_description}场景5')
        payload = ParameterSetting(self.payload).empty_value('seller_name')
        traceId = get_traceId(self._testMethodName)
        headers = self.ch.set_headers(traceId=traceId)
        response = self.ch.post(self.url, headers, payload)
        self.assertEqual(response['register_code'], 'E01')

    def test_06(self):
        print(f'{self.case_description}{self.function_description}场景6')
        payload = ParameterSetting(self.payload).customize_value({'seller_name': '我是不存在的商家名称'})
        traceId = get_traceId(self._testMethodName)
        headers = self.ch.set_headers(traceId=traceId)
        response = self.ch.post(self.url, headers, payload)
        self.assertEqual(response['register_code'], 'E01')

    def test_07(self):
        print(f'{self.case_description}{self.function_description}场景7')
        payload = ParameterSetting(self.payload).empty_value('ouid')
        traceId = get_traceId(self._testMethodName)
        headers = self.ch.set_headers(traceId=traceId)
        response = self.ch.post(self.url, headers, payload)
        self.assertEqual(response['register_code'], 'E01')

    def test_08(self):
        print(f'{self.case_description}{self.function_description}场景8')
        payload = ParameterSetting(self.payload).customize_value({'mobile': '', 'mix_mobile': ''})
        traceId = get_traceId(self._testMethodName)
        headers = self.ch.set_headers(traceId=traceId)
        response = self.ch.post(self.url, headers, payload)
        self.assertEqual(response['register_code'], 'E01')

大家可以看到,测试代码中,基本上不会看到参数会有写死为具体数字、字符串等数据结构的情况

三、处理要测试接口的入参参数值来源于一个或多个其它接口的场景,对应关联接口

关于这一点,我想说的是关联接口是一种错误的说法,在接口测试实际中,你要测试的接口始终只有1个,而你要测试这个接口的入参的参数值,可能来源于请求其它1个或多个接口之后来获取。如果关联接口这种说法成立,那么登录接口获取的业务接口访问的身份认证信息是所有业务接口必须的,那登录接口岂不是所有业务接口的关联接口?

那么怎么来理解这种测试场景呢,很简单,测试接口之前请求的接口,你可以把它理解为测试接口的测试数据准备,基于这种上下游关系,我从代码的方式给它命名为接口上下文,比较形象,当然你也可以随意命名,先给大家展示下测试代码中如何实现接口上下文的效果,至于如何设计将在unittest/pytest+requests+htmltestrunner/allure怎样封装requests库?给大家详细讲解

@mark(value='smoke')
    def test_01(self):
        print(f'{self.case_description}{self.function_description}场景1')
        am = AliMember()
        mobile = am.mobile
        mix_mobile = am.mix_mobile
        payload = ParameterSetting(self.payload).customize_value({'seller_name': AliMember().seller_name, 'mix_mobile': mix_mobile, 'mobile': mobile})
        traceId = get_traceId(self._testMethodName)
        headers = self.ch.set_headers(traceId=traceId)
        self.ch.post(self.url, headers, payload, context=True, context_apiName='我是上游接口1', context_description='需要获取我的xx参数值作为测试接口入参')
        self.ch.post(self.url, headers, payload, context=True, context_apiName='我是上游接口2', context_description='需要获取我的xx参数值作为测试接口入参')
        response = self.ch.post(self.url, headers, payload)
        self.assertEqual('SUC', response['register_code'])

上面上下文接口的执行效果:

测试报告里一致的效果:

总结:希望jmeter老玩家在传道授业解惑的时候从接口测试的本质工作去宣传这些概念,说明这只是具体到jmeter解决方案的概念,不然只会对入门的新手带来认知上的混淆,误人子弟

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值