最近项目要求快速完成接口自动化测试,因时间有限,经过考察后决定使用开箱即用的 httprunner,在使用过程同时也顺便总结了一些较为常见的问题和用法,在此记录一下。

为避免业务信息泄漏,文中某些 api 例子使用 https://httpbin.org/,后续使用过程中如有新的内容,将持续更新此文。

https 请求证书验证

在对 https 接口进行测试时如果请求经过代理则可能会有 certificate verify failed 的报错,如:

ERROR: test_0000_000 (httprunner.api.TestSequense)
---------------------------------------------------------------------
Traceback (most recent call last):
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)

原因是 request 模块发送请求函数有个参数 verify 值默认为 True.
使用 httprunner 则可在用例 yml 或 json 文件中将 config 或 teststep 中该参数设置为 False 跳过证书验证。

-   config:
        ...
        verify: False
-   test:
        ...
        request:
            verify: False

官方 changlog 中也描述过修复之后的这两处设置优先级:

## 2.0.3 (2019-02-24)
Bugfixes

代理调试

httprunner 库本身没有提供设置代理的接口,但是底层使用了 urllib.requests 等库,可以设置 HTTP_PROXY 和 HTTPS_PROXY 环境变量,常用的网络库会自动识别这些环境变量,使用变量设置的代理发起请求:

httprunner 本身支持多种环境变量设置与读取方式,用法参考:https://cn.httprunner.org/prepare/dot-env/

e.g. 日常调试使用代理(如 charles 等工具)可在 debugtalk.py 开头加上

import os

os.environ['http_proxy'] = 'http://127.0.0.1:8888'
os.environ['https_proxy'] = 'https://127.0.0.1:8888'

$ 符引用

在 httprunner 中,$ 符被用来引用变量或函数,如果遇到需要将 $ 当成一个普通字符来使用时则可以通过 $$ 来表示一个普通 $ 字符而不是引用。

参考源码 httprunner/parser.py 第 598~603 行:

# content is in string format here
if not is_var_or_func_exist(content):
    # content is neither variable nor function
    # replace $$ notation with $ and consider it as normal char.
    # e.g. abc => abc, abc$$def => abc$def, abc$$$$def$$h => abc$$def$h
    return content.replace("$$", "$")

e.g.

# 在下面的 testcase yaml 文件中,command 变量中的 $$5 则代表 $5 ,否则只写为 $5,解析时则会去找变量名为 5 的变量,如果该变量不存在,则会报错。
- config:
    name: xxxx

- test:
    name: xxxx
    api: api/xx/xxxx.yaml
    variables:
      command: ifconfig | awk '{print $$5}'

Issue
但在这里,我也遇到一个问题,当我把变量设置放在 - config 中时,如:

- config:
    name: xxxx
    variables:
      command: ifconfig | awk '{print $$5}'

- test:
    name: xxxx
    api: api/xx/xxxx.yaml

$$ 并没有被当成普通的 $,而是报错变量未找到

  File "/Users/mac/application/miniconda/envs/httprunner/lib/python3.7/site-packages/httprunner/parser.py", line 508, in __parse
    raise exceptions.VariableNotFound(var_name)
httprunner.exceptions.VariableNotFound: 5

该问题已向代码库提交 Issue: https://github.com/httprunner/httprunner/issues/657

Json 响应中数组的提取和断言

对于包含数组的 Json 响应,如下

# api 地址:https://httpbin.org/#/Response_formats/get_json

{
  "slideshow": {
    "author": "Yours Truly",
    "date": "date of publication",
    "slides": [
      {
        "title": "Wake up to WonderWidgets!",
        "type": "all"
      },
      {
        "items": [
          "Why <em>WonderWidgets</em> are great",
          "Who <em>buys</em> WonderWidgets"
        ],
        "title": "Overview",
        "type": "all"
      }
    ],
    "title": "Sample Slide Show"
  }
}

如果需要在 extract 或 validate 中对 "title": "Wake up to WonderWidgets!" 字段进行断言,提取方式可为 content.slideshow.slides.0.title(其中数字 0 为取数组中的第 1 位,序号以 0 开始):

e.g.

- config:
    name: testcase description
    verify: False

- test:
    name: /json
    request:
      method: GET
      url: https://httpbin.org/json
    extract:
      title: content.slideshow.slides.0.title
    validate:
      - eq:
          - status_code
          - 200
      - eq:
          - headers.Content-Type
          - application/json
      - eq:
          - content.slideshow.slides.0.title
          - 'Wake up to WonderWidgets!'

text/html 响应的提取和断言

对于文本内容和 html 响应需要对关键字进行提取和断言,可使用正则表达式进行匹配查找,有 html 响应内容如下:

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
      <h1>Herman Melville - Moby-Dick</h1>

      <div>
        <p>
          Availing himself of the mild, summer-cool weather that now reigned in these latitudes, and in preparation for the peculiarly active pursuits shortly to be anticipated, Perth, the begrimed, blistered old blacksmith, had not removed his portable forge to the hold again, after concluding his contributory work for Ahab's leg, but still retained it on deck, fast lashed to ringbolts by the foremast;        
        </p>
      </div>
  </body>

需要对文中关键字 summer-cool 进行断言,则可以使用正则表达式分组的方式对单词进行提取:

- config:
    name: testcase description
    verify: False

- test:
    name: /html
    request:
      method: GET
      url: https://httpbin.org/html
    extract:
      key: Availing himself of the mild, (.+) weather that now reigned in these latitudes
    validate:
      - eq:
          - status_code
          - 200
      - eq:
          - headers.Content-Type
          - text/html; charset=utf-8
      - eq:
          - $key
          - summer-cool

这个方法原理是通过正则分组,默认提取第一个分组匹配到的内容,用法类似于 loadrunner 的左右边界提取。

testcase 之间传递参数

使用 output/export 和 extract 可以在 testcase 之间传递参数
testcase 级的变量:

可以在 testcase 的 - config 中通过 output 或者 export (2.2.2 版本添加) 将其暴露,然后在该 testcase 被引用时通过 extract 将变量提取出来使用。

- config:
    name: testcase description
    verify: False
    variables:
      configVar: configVar
    export:
      - key
      - configVar
      - teststepVar

- test:
    name: /html
    request:
      method: GET
      url: https://httpbin.org/html
    variables:
      teststepVar: teststepVar
    extract:
      key: Availing himself of the mild, (.+) weather that now reigned in these latitudes
    validate:
      - eq:
          - status_code
          - 200
      - eq:
          - headers.Content-Type
          - text/html; charset=utf-8

# hrun output message
WARNING  variable 'teststepVar' can not be found in variables mapping, failed to export!
INFO     
==================== Output ====================
Variable         : Value
---------------- : -----------------------------
key              : summer-cool
configVar        : configVar
------------------------------------------------

在 teststep 中引用以上 testcase:

- config:
    name: testcase description
    verify: False

- test:
    name: /html
    testcase: testcases/html.yaml
    extract:
      - key
      - configVar
    validate:
      - eq:
          - $key
          - summer-cool

- test:
    name: /json
    request:
      method: GET
      url: https://httpbin.org/json
    validate:
      - eq:
          - $configVar
          - configVar

2.2.2 版本 CHANGELOG 中对此处用法做了相关说明:

## 2.2.2 (2019-06-26)
Changed

复用 cookies 和 token

很多时候测试 api ,我们并不希望频繁登录获得授权,这个时候就需要复用 cookies 和 token 。

1. 每个 testcase 登录一次

在 httprunner 中如果不同 api 是在同一个 testcase 的不同步骤,那么只需要在一个 teststep 登录授权,则其他的 teststep 是可以共用授权,推测应该是 testcase 里共用同一个 requests.seesion 。

2. 将 cookies 或 token 写入文件,读取时按需刷新

cookies

在 debugtalk.py 文件中增加两个函数,generate_cookies 用于在读取文件中的 cookies,teardown_saveCookies 用于在 login.yaml 中保存 cookies 内容到文件。

import time
import requests
from httprunner.api import HttpRunner

COOKIES_PATH = r'xxx/xxx/cookies'

def generate_cookies():
    if (not os.path.exists(COOKIES_PATH)) or (
            3600 < int(time.time()) - int(os.path.getctime(COOKIES_PATH))):
        # cookies 文件不存在 或 最后修改时间超过 3600 秒 (1小时) 则重新登录刷新 cookies
        runner = HttpRunner()
        runner.run(r'api/login.yaml')
    with open(COOKIES_PATH, 'r') as f:
        cookies: str = f.read()
    return cookies

def teardown_saveCookies(response: requests.Response):
    """保存 cookies 到文件给其他 api 调用"""
    cookies: dict = response.cookies
    foo: list = []
    # 遍历 cookies 拆分 dict 并拼接为特定格式的 str
    # 如: server=xxxxx; sid=xxxxxx; track=xxxxx; 
    for k, v in cookies.items():
        foo.append(k + '=' + v + '; ')
    bar: str = "".join(foo)
    with open(COOKIES_PATH, 'w') as f:
        f.write(bar)

具体调用如下:

# login.yaml

name: login
base_url: http://xxxxxxx

request:
  method: POST
  url: /login
  data:
    uid: xxx
    password: xxx

teardown_hooks:
  - ${teardown_saveCookies($response)}


# other api or testcase

name: other api
base_url: http://xxxxxxx

request:
  method: GET
  url: /others
  headers:
    Cookie: ${generate_cookies()}

token

同理,token 的保存和读取也大同小异:

import time
import json
import requests
from httprunner.api import HttpRunner

AUTHORIZATION_PATH = r'xxx/xxx/authorization'

def generate_authorization():
    if (not os.path.exists(AUTHORIZATION_PATH)) or (
            3600 < int(time.time()) - int(
        os.path.getctime(AUTHORIZATION_PATH))):
        # authorization 文件不存在或最后修改时间超过 3600 秒(1 个小时)则重新登录
        runner = HttpRunner()
        runner.run(r'api/login.yaml')
    with open(AUTHORIZATION_PATH, 'r') as f:
        authorization: str = f.read()
    return authorization

def teardown_saveAuthorization(response: requests.Response):
    foo: dict = json.loads(response.text)
    bar: str = "Bearer " + foo['data']['token']
    # 保存 authorization 到文件给其他 api 调用
    with open(AUTHORIZATION_PATH, 'w') as f:
        f.write(bar)

这里生成 cookies 和 token 直接用的是 httprunner 的 api ,当然也可以直接使用 requests 拼装登录授权的接口来获取 cookies 或 token。

++ 未完待续 ++

原文链接:https://www.luckycoding.com/2019/07/12/httprunner-summarize


↙↙↙阅读原文可查看相关链接,并与作者交流