接口测试 pytest 框架 +yaml 文件驱动 轻松接口自动化

Elsie · 2019年09月27日 · 最后由 为了美好的老年生活努力 回复于 2022年07月01日 · 4800 次阅读

去年开始做测试,一开始接触到的接口自动化测试 是 unittest+excel 的那种方式,我不喜欢 excel(纯个人喜好) 尤其是量大的时候 感觉用起来不是很顺手。后来接触到 pytest 便很喜欢,结合公司业务用 pytest+ATX 开发了 ui 自动化 以及用 selenium 开发了 web ui 自动化
前者没有应用起来,后者还是很稳定的,回头想想 还是想再做一下接口测试 毕竟接口还是性价比最高的,我现在分享的这个是基于一位前辈分享的基础上进行的改进。只是忘了那位前辈是谁了 真不好意思,此分享当是自己的一个学习记录吧

先看下 yaml 文件的设计 yaml 文件我也比较喜欢 可以用& * 等符合定义和引用变量 很方便 这样这个接口数据会比较少

---
Test:
 desc: "登录"
 parameters:
  -
    #测试登录
    desc: 测试登录
    method: post
    url: /gw-driver/zhuanche-driver/driver/login
    data:
       -
      request-ts: '1574940616636'
      sign: 0841B69FB087643979BF464E34350AA3
      type: '2'
    ··-
      request-ts: '1574940616636'
      sign: 0841B69FB087643979BF464E34350AA3
      type: '1'
    header: &header
        car-pf: ANDROID_DRIVER
        car-ps: shouqi
        car-sv: 8.1.0
        car-mv: OPPO OPPO PACM00
        appCode: android_special_driver
    assert_code: 200
    assert_in_text: 登录成功
    assert_contents:
       code: 0

1:如何获取 yaml 文件中的数据 这个真的够啰嗦

class GetData():


  def __init__(self,file_name):
      '''
      获取filename.yaml 配置文件的所有数据
      '''
      self.desc = []
      self.method = []
      self.url = []
      self.data = []
      self.header = []
      self.assert_code = []
      self.assert_text = []
      self.assert_in_text = []
      self.assert_body_has = []
      log.info('解析yaml文件 path:' + str(path) + ' Param/Yaml/'+file_name+'.yaml')
      param = get_param(file_name)
      for i in range(0, len(param)):
          self.method.append(param[i]['method'])
          self.url.append(param[i]['url'])
          self.header.append(param[i]['header'])
          if 'desc' in param[i]:
              self.desc.append(param[i]['desc'])
          else:
              self.desc.append(None)
          if 'data' in param[i]:
              self.data.append(param[i]['data'])
          else:
              self.data.append(None)
          if 'assert_code' in param[i]:
              self.assert_code.append(param[i]['assert_code'])
          else:
              self.assert_code.append(None)
          if 'assert_text' in param[i]:
              self.assert_text.append(param[i]['assert_text'])
          else:
              self.assert_text.append(None)
          if 'assert_body_has' in param[i]:
              self.assert_body_has.append(param[i]['assert_body_has'])
          else:
              self.assert_body_has.append(None)
              # 断言数据列表化
          if 'assert_in_text' in param[i]:
              text = param[i]['assert_in_text']
              Ltext = list(text.split(' '))
              self.assert_in_text.append(Ltext)
          else:
              self.assert_in_text.append(None)

我以抛出问题的方式讲述吧
2:将接口数据写到 yaml 文件中 然后读取测试其实很简单 和 excel 操作一样,只需要封装一些操作 yaml 文件的函数即可.但是如果用 pytest 框架将如何实现多个接口驱动呢?
毕竟有很多接口其实都没有依赖性的 我们没有必要一个接口写一个 test 那样效率太堪忧 其实基于 pytest 的 parameterize 特性 一切就可以解决了

#urls 为读取到yaml文件中的所有url type:list 如何获取数据 查看 title1
 @pytest.mark.parametrize('url',urls)
    def test_home_page(self,url):
        '''
        :param url:
        :return:
        '''
        request = Request.Request(res)

        #获取测试数据在yaml文件中的索引 关于重复的url目前还没有解决 所以只支持不同的url
        index=urls.index(url)
        log.info('测试接口描述:'+str(descs[index])) #打印日志

        response=request.send_request(methods[index],urls[index],params[index],headers[index])

        assert test.assert_code(response['code'], assert_codes[index])
        if assert_in_texts[index] is not None:
            assert test.assert_in_text(response['body'],*assert_in_texts[index])


 @pytest.mark.parametrize('url',base_conf.urls)
    def test_home_page_config(self,get_init_data,url):
        '''
        测试首页单接口
        :param get_init_data:
        :param url:
        :return:
        '''

        res=get_init_data
        request = Request.Request(res)
        #获取测试数据在yaml文件中的索引
        index=base_conf.urls.index(url)
        if base_conf.run[index] == False:
            pytest.skip('skip')
            base_conf.log.info('测试接口描述:'+str(base_conf.descs[index]))

        replace_data(res,base_conf.headers[index],base_conf.params[index])

        #处理一个接口有多个用例数据
        if type(base_conf.params[index])==list:
            for i in base_conf.params[index]:
                response=request.send_request(base_conf.methods[index], base_conf.urls[index], i, base_conf.headers[index])
                assert base_conf.test.assert_full(response,**{'assert_codes':base_conf.assert_codes[index],
                                                              'assert_in_texts':base_conf.assert_in_texts[index],'assert_content':base_conf.assert_content[index]})

        else:
            response=request.send_request(base_conf.methods[index],base_conf.urls[index],base_conf.params[index],base_conf.headers[index])
            assert base_conf.test.assert_full(response,
                                    **{'assert_codes': base_conf.assert_codes[index], 'assert_in_texts': base_conf.assert_in_texts[index],
                                       'assert_content': base_conf.assert_content[index]})

3: 接上个问题 有关联的接口该怎么办 我还没有什么明智的解放 那就只能一个一个处理了,但是有关联的接口也可以再划分 比如登录接口,那是所以接口都会依赖的 主要是取其 token 数据 那就把它放到 conftest 中 登录一次 返回 token 即可

conftest.py
global res
@pytest.fixture(scope='session',autouse=True)
def login():
    global res
    res={}

    #定义环境
    env=Consts.API_ENVIRONMENT_DEBUG
    index = descs.index(Consts.TEST_LOGIN)

    res['env'] = env
    res['token'] = ''
    request = Request.Request(res)

    log.info('---------------测试初始化--登录---------------')
    response = request.send_request(methods[index], urls[index], params[index], headers[index])

    log.info('接口返回结果  ' + str(response))
    assert test.assert_code(response['code'], assert_codes[index])
    assert test.assert_in_text(response['body'],*assert_in_text[index])

    body=response['body']  #此处有封装成一个查找response的函数 工具函数最后分享
    data=body['data']
    tok=data['token']
    res['env']=env
    res['token']=tok

@pytest.fixture(scope='session')
def get_init_data():
    '''
    获取测试环境 以及司机token
    :return: dict
    '''
    global res
    return res


4:如何断言 前辈的 demo 中 有二种比对 比对全部 json 传 比对包含的数据 比对全部数据 使用情况极少,因为很少接口返回每次都一样的数据, 比对包含的字符我又用 我又加了一种 那就是比对 key-value,用到了 我上边说的工具函数

在 yaml 文件中写法

assert_code: 200 
assert_in_text: 登录成功
assert_contents:
   code: 0
   data: 1

断言代码

def assert_in_text(self,body,*expect_data):
    '''
    验证 body中是否包含预期字符串
    :param body:
    :param expect_data:
    :return:
    '''
    #text = json.dumps(body, ensure_ascii=False)
    text=str(body)
    try:
        for i in expect_data:
            assert i in text
        return True
    except:
        self.log.error("Response body != expected_msg, expected_msg is %s, body is %s" % (expect_data, body))
        raise

def assert_content(self,body,**expectes_msg):

    try:
        for key,value in expectes_msg.items():
            res=get_target_value(key,body,[])
            assert res[0]==str(value)
        return True
    except:
        self.log.error("Response body != expected_msg, expected_msg is %s, body is %s" % (expectes_msg, body))
        raise

工具函数

def get_target_value(key, dic, tmp_list):
    """
    :param key: 目标key值
    :param dic: JSON数据
    :param tmp_list: 用于存储获取的数据
    :return: list
    """
    if type(dic)!=dict or type(tmp_list)!=list:
        return 'argv[1] not an dict or argv[-1] not an list '
    if key in dic.keys():
        tmp_list.append(dic[key])  # 传入数据存在则存入tmp_list
    else:
        for value in dic.values():  # 传入数据不符合则对其value值进行遍历
            print(type(value))
            if type(value)==dict:
                get_target_value(key, value, tmp_list)  # 传入数据的value值是字典,则直接调用自身
            elif isinstance(value, (list, tuple)):
                _get_value(key, value, tmp_list)  # 传入数据的value值是列表或者元组,则调用_get_value
    return tmp_list

5:测试中总不可能只涉及到一个被测系统 比如需要修改一些配置等 那肯定要设计到后台的一些功能 当然用 selenium 的方式也可以实现 这里还是统一风格 用接口吧
现在公司内网所有的平台都是用 sso 登录的 所以就遇到怎么用 sso 实现登录

def login():
    '''
    登录
    :return:
    '''
    # 获取接口在yaml文件中的索引
    index = descs_admin.index(Consts.ADMIN_LOGIN)
    api_url = req_url_admin + urls_admin[index]
    log.info('admin登录')
    log.info(api_url)
    '''登录/a'''
    session=HTMLSession()
    response=session.get(api_url,headers=headers_admin[index])

    #获取sso重定向地址
    api_url=response.url
    It=response.html.xpath('//input[@name="lt"]/@value')[0]
    execution=response.html.xpath('//input[@name="execution"]/@value')[0]
    print(It)
    params_admin[index]['lt']=It
    params_admin[index]['execution'] = execution
    headers_admin[index]['Referer'] = api_url
    '''登录sso'''
    res=session.post(api_url,params_admin[index],headers_admin[index])
    assert res.status_code==200
    return session

@pytest.fixture()
def test():
    '''
    :return:
    '''
    request=admin_login()
    # 获取接口在yaml文件中的索引
    index = descs_admin.index(Consts.Test)
    api_url = req_url_admin + urls_admin[index]
    if methods[index] == 'post':
        response = request.post(api_url, params_admin[index], headers_admin[index])
    print(response)
    log.info(str(response))
    assert test.assert_code(response.status_code, assert_codes_admin[index])

6: pytest-html 默认的报告模式无法满足我 ,我只想保留错误接口的日志输出 ,想要添加一些 description 来说明接口测试的流程 所以需要在最外层的 conftest.py 中添加两个 hook

#使用日志为空的通知替换所有附加的HTML和日志输出
@pytest.mark.optionalhook
def pytest_html_results_table_html(report, data):
    if report.passed:
        del data[:]

@pytest.mark.optionalhook
def pytest_html_results_table_header(cells):
    cells.insert(2, html.th('Description'))
    cells.insert(3, html.th('Time', class_='sortable time', col='time'))
    cells.pop()

7:用例不通过 如何快速定位 当日是通过详细的 log 了 我的 log 信息 记录了每一个接口的输入输出 以及对用例步骤进行了日志的区分 如下
虽然很简单 但是可以让你的日志看起来相当舒服 整个流程也一目了然

@pytest.fixture(scope='module',autouse=True)
def log_init():
    log.info('---------------TestAfterMarket-TestRefundAfArrive CASE1 测试功能1 START-----------')
    yield
    log.info('---------------TestAfterMarket-TestRefundAfArrive CASE1 测试功能2 END-----------')

共收到 16 条回复 时间 点赞

展示的代码总,部分内容好像在哪里看岛国,再说语法和风格还可以再提炼提炼。

该死的输入法,打字太快了,更正下:展示的代码中,部分内容好像在哪里看到过,再说语法和风格还有可以提炼提炼的地方。

看过对啊 我说过我是基于前辈的做的改进啦

不错,pytest 比 unittest 灵活,Excel 存放 case,操作太麻烦了

Elsie #12 · 2019年10月10日 Author
aibreze 回复

同感

get_param 这个函数里面是啥

Elsie #10 · 2019年11月20日 Author
回复

返回单个 yaml 文件 的所有 parameters 信息

可以给下 github 项目地址吗,想学习下🙏

web ui 框架,有 git 可以分享吗

怎么能让普通函数可以得到 fixture 的返回结果呢?

Elsie #11 · 2020年09月01日 Author
TI幺 回复

可以用全局变量 或者写入文件的方式

Elsie 回复

最后我用了很笨的办法解决了,但是我觉得肯定有很好的办法解决,但是等不了了。谢谢你的分享。

请问楼主,get_param()实现与 with open (“”, encoding="utf-8") as f: 有什么区别吗

Elsie #14 · 2020年12月28日 Author

get_param 是封装了对 yaml 文件的读取和拆分,当中是有用到 python 的 open 方法的哈

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