​​pytest+yaml+allure接口自动化测试框架

高薪必备!18K接口自动化测试框架落地全流程|零基础到实战通关

前言#

自动化测试,是目前测试行业一项比较普遍的测试技术了,之前的以UI自动化测试为主,现在的以接口自动化测试为主,无论技术更迭,自动化测试总有他的重量,用机器代替手工工作,是21世纪不断进行的课题。

​ 可是身为测试,难受的是脚本容易写,学几天python,照猫画虎三两天也能写一个不错的脚本。可是想更上一层,去搭建一个测试框架却显得不是那么容易,曾经我也是这样的困难。时光不负有心人,学习了漫长时间终于是现在有了一些开发基础,抽空搞了一个简单版本的接口自动化测试框架。

希望我的框架能给予你一定启发的同时,你也能指出一些我的不足之处,互相学习,我们才能共同进步。

环境搭建#

目录文件添加#

我们打开vscode新建一个项目,名字就姑且命名为:interface_test_example, 创建好之后,我们就按照这个下面这个目录结构去创建相应的文件内容。

.
├── common                    	——公共方法目录
│   ├── cache.py				——缓存文件
│   ├── exceptions.py			——异常处理
│   ├── __init__.py				
│   ├── json.py					——序列化和反序列化
│   ├── regular.py				——正则处理
│   ├── request.py				——请求处理
│   └── result.py				——响应处理
├── conftest.py					——pytest胶水文件
├── environment.properties		——allure配置文件
├── logs						——日志目录
├── main.py						——主运行文件
├── pytest.ini					——pytest配置文件
├── readme.md					
├── requirements.txt	
├── tests						——测试用例目录
│   └── testcase.yaml
└── utils						——第三方工具文件├── __init__.py	├── logger.py				——日志├── readme.md└── time.py					——时间处理

当你把上面这些内容创建完成之后我们的项目内容就算整体创建完成了。

python虚拟环境创建#

在创建之前我先声明一下我所使用的python版本是3.8.6版本。学习本篇请不要使用3.8版本以下python,某些语法会不支持。

1、创建虚拟环境

python3 -m venv env

2、 安装requirements.txt的依赖包

pip install -r requirements.txt

requirements.txt的具体内容

allure-pytest==2.9.43
allure-python-commons==2.9.43
pytest==6.2.5
pytest-assume==2.4.3
pytest-html==3.1.1
PyYAML==5.4.1
requests==2.26.0

安装完成之后我们的环境就搭建好了。

测试用例管理#

excel这种总归是太麻烦了,所以我们需要一个更合适的。挑来选去yaml是最简单方便的,数据能几乎无缝切换。

先来看看我们的用例吧,都写了些什么。打开tests/testcase.yaml文件,输入以下内容。

config: # 测试信息baseurl: "https://www.zhixue.com"timeout: 30.0headers:Accept: application/json, text/javascript, */*; q=0.01Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Connection: keep-aliveUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36cookies: aliyungf_tc=AQAAANNdlkvZ2QYAIb2Q221oiyiSOfhg; tlsysSessionId=cf0a8657-4a02-4a40-8530-ca54889da838; isJump=true; deviceId=27763EA6-04F9-4269-A2D5-59BA0FB1F154; 6e12c6a9-fda1-41b3-82ec-cc496762788d=webim-visitor-69CJM3RYGHMP79F7XV2M; loginUserName=18291900215X-Requested-With: XMLHttpRequest
variable:none : none
tests:test_login:description: "登录"method: postroute: /weakPwdLogin/?from=web_loginRequestData:data:loginName: 18291900215password: dd636482aca022code:description: encryptValidate:expectcode: 200resultcheck: '"result":"success"'regularcheck: '[\d]{16}'Extract:- datatest_login_verify:description: "验证登录"method: postroute: /loginSuccess/RequestData:data:userId: "${data}"Validate:expectcode: 200regularcheck:resultcheck: '"result":"success"'

第一部分config内容:主要是一些全局的配置信息,如请求地址、请求头等。

第二部分variable内容:主要是预先设置一些全局变量等等内容。比如可以加入邮箱地址等等。

第三部分tests内容:这个是真正的测试用例部分,通过匹配requests库的输入参数,以简洁明了的写法更好的支持测试。

日志封装#

打开utils/logger.py文件,这个utils的意思是一个工具包的意思。在这个里面我们主要存放可以独立运行的工具模块。比如日志文件就是一个可以独立运行的。打开之后我们输入以下的内容:

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
日志类
"""
import os
import logging
from logging.handlers import RotatingFileHandlerdef init_logger():"""初始化日志"""basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))debug_file = os.path.join(basedir, 'logs', 'server.log')logger_formatter = logging.Formatter('%(levelname)s %(asctime)s [%(filename)s:%(lineno)s] %(thread)d %(message)s')# debuglogger_debug = logging.getLogger('apitest')handler_debug = RotatingFileHandler(debug_file,encoding='utf-8',maxBytes=20 * 1024 * 1024,backupCount=10)handler_debug.setFormatter(logger_formatter)logger_debug.setLevel(logging.DEBUG)logger_debug.addHandler(handler_debug)# 在控制台输出return logger_debuglogger = init_logger()if __name__ == '__main__':logger.debug("debug")logger.info("info")logger.warning('warning')logger.error("error")logger.critical('critical')

下面一些日志输入示例。我们来执行一下。

image-20210924220036286

可以看到成功的在日志文件中写入了新的信息。

缓存工具#

是的你没看错,我给它起的名字就叫缓存,其实内部组成本质就是一个python字典。而不是你想的redis这种。

打开common/cache.py文件,我们输入以下内容。

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
缓存类
"""
from collections import UserDictclass CachePool(UserDict):"""全局变量池"""def get(self, key, default=None):return self.data.get(key, default)def set(self, key, value = None):self.data.setdefault(key, value)def has(self, key):return key in self.datadef __len__(self):return len(self.data)def __bool__(self):return bool(self.data)cache = CachePool()if __name__ == '__main__':cache.set('name', 'wxhou')print(len(cache))print(cache.get('name'))

我们执行测试一下:

image-20210924221323950

可以看到没有问题。通过这个字典我们把一些临时的信息放在这个里面,因为只是示例项目,用redis显得有些麻烦,采用这种方式更为简便一些。

读取yaml测试用例#

​ 使用yaml作为测试用例,我们就需要对文件的内容进行读取,常规来说的应该是通过pyyaml对读取到的内容进行数据解析,然后使用pytest parametrize参数化功能进行数据参数化用例测试。但是完事之后,这样的方式好像不是很优雅,写的代码组织起来比较费劲,于是乎,我在pytest的官方文档中,发现了一套更为一套非常优雅的测试执行方式,他们称之为non-python test的测试模式。

具体内容可以查看官方文档,感兴趣的可以去看看:Working with non-python tests — pytest documentation

# content of conftest.py
import pytestdef pytest_collect_file(parent, path):if path.ext == ".yaml" and path.basename.startswith("test"):return YamlFile.from_parent(parent, fspath=path)class YamlFile(pytest.File):def collect(self):# We need a yaml parser, e.g. PyYAML.import yamlraw = yaml.safe_load(self.fspath.open())for name, spec in sorted(raw.items()):yield YamlItem.from_parent(self, name=name, spec=spec)class YamlItem(pytest.Item):def __init__(self, name, parent, spec):super().__init__(name, parent)self.spec = specdef runtest(self):for name, value in sorted(self.spec.items()):# Some custom test execution (dumb example follows).if name != value:raise YamlException(self, name, value)def repr_failure(self, excinfo):"""Called when self.runtest() raises an exception."""if isinstance(excinfo.value, YamlException):return "\n".join(["usecase execution failed","   spec failed: {1!r}: {2!r}".format(*excinfo.value.args),"   no further details known at this point.",])def reportinfo(self):return self.fspath, 0, f"usecase: {self.name}"class YamlException(Exception):"""Custom exception for error reporting."""

可以看到官方文档中以极其优雅的方式通过yaml文件驱动了两个测试用例。我们也将在此基础上进行扩展衍生。

我们根据官方文档中的示例文件,在这个基础上进行修改,加入我们的内容。

pytest_collect_file#

首先我们修改pytest_collect_file函数中的内容,让他支持yamlyml两种格式的文件内容。因为这两种都可以,官网示例中只有一个。

if path.ext in (".yaml", ".yml") and path.basename.startswith("test"):return YamlFile.from_parent(parent, fspath=path)
YamlFile.collect#

接下来修改我们的YamlFile.collect方法,这里面就是对读出来的详细内容按照设置的格式进行处理,该存入缓存的放入缓存,该执行测试的时候执行测试。

if not any(k.startswith('test') for k in raw.keys()):raise YamlException("{}yaml non test found".format(self.fspath))

通过这个语句我们先判断一下,有没有测试用例,如果没有测试用例我们直接就报错了,不在执行,抛出异常,这个异常需要我们自己封装一下。我们打开common/exceptions.py文件。输入以下内容:

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
异常类
"""
from requests.exceptions import RequestExceptionclass YamlException(Exception):"""Custom exception for error reporting."""def __init__(self, value):self.value = valuedef __str__(self):return "\n".join(["usecase execution failed","   spec failed: {}".format(self.value),"   For more details, see this the document.",])

这个就是当我们发现yaml文件中没有符合的测试标签内容后抛出的异常类。

然后我们接着先读取全局变量:

if variable := raw.get('variable'):for k, v in variable.items():cache.set(k, v)

我们把yaml文件中预设的全局变量信息中全部存在我们设置的缓存模块中,这样在测试过程中我们可以随时的去用。

继续读取配置文件。

if config := raw.get('config'):   for k, v in config.items():cache.set(k, v)

然后我们读取常用的测试信息也放入缓存之中,方便运行过程中随时去调用。

最后我们来处理一下。测试用例部分:

if tests := raw.get('tests'):for name, spec in tests.items():yield YamlTest.from_parent(self,name=spec.get('description') or name,spec=spec)

可以看到,在官方文档中使用了sorted函数进行了再次排序。我这里没有是因为再次排序会破坏用例的结构和顺序。最后输出的时候spec.get('description') or name的写法先获取yaml文件中我们设置的中文标识,如果中文标识不存在则继续使用英文标识。其余和官方文档保持一致。

以上就是做出的改动,我们来看看吧:

import yaml
import pytest
from common.cache import cache
from common.exceptions import YamlExceptiondef pytest_collect_file(parent, path):if path.ext in (".yaml", ".yml") and path.basename.startswith("test"):return YamlFile.from_parent(parent, fspath=path)class YamlFile(pytest.File):def collect(self):raw = yaml.safe_load(self.fspath.open(encoding='utf-8'))if not any(k.startswith('test') for k in raw.keys()):raise YamlException("{}yaml non test found".format(self.fspath))if variable := raw.get('variable'):for k, v in variable.items():cache.set(k, v)if config := raw.get('config'):for k, v in config.items():cache.set(k, v)if tests := raw.get('tests'):for name, spec in tests.items():yield YamlTest.from_parent(self,name=spec.get('description') or name,spec=spec)

站在巨人的肩膀上才能看得更远。在pytest non-python tests的内容之上做了一些改动,使得读取文件更加贴合我们定义的yaml文件内容。在精简了很多代码的同时我们也达到了预期的效果。

处理request#

​ 谈到HTTP请求,我们首先就会想到requests库,这个第三方库,以极其优雅的封装方式和简易的写法,在python界有着重要的地位,在这个接口自动化测试框架中,我们也会使用这个库进行二次封装。让其融入到我们的测试框架中来。

​ 对于这个库我就不做过多的介绍了,之前有一篇文章专门介绍,感兴趣可以点击:python requests由浅入深 - 随风挥手 - 博客园 (cnblogs.com)

执行测试的代码#

上一章节已经讲了怎么读取测试用例数据,根据pytest官网的non-python test内容,我们还需要编写一个YamlTest类来执行测试。

继续打开conftest.py文件,在里面加上如下内容:

# +++
from common.request import HttpRequest
from common.exceptions import RequestException# +++class YamlTest(pytest.Item):def __init__(self, name, parent, spec):super(YamlTest, self).__init__(name, parent)self.spec = specself.request = HttpRequest(exception=(RequestException, Exception))def runtest(self):"""Some custom test execution (dumb example follows)."""self.request.send_request(**self.spec)def repr_failure(self, excinfo):"""Called when self.runtest() raises an exception."""logger.critical(excinfo.value)logger.critical(excinfo.traceback[-6:-1])            def reportinfo(self):return self.fspath, 0, f"usecase: {self.name}"

通过继承pytest.Item类我们可以使用父类的运行测试的方法来执行测试。

__init__方法#

在这个里面我们接收来自yamlfile类中collect方法的yield生成器传给我们的测试数据。

runtest#

继承父类的runtest方法我们可以在这个里面执行我们的测试,把接受到的参数传入我们二次封装的HttpRequest类,就可以对我们在yaml文件中添加的接口进行测试了。

repr_failure#

如果在运行中发生了用例失败的现象我们可以在这个方法中拦截并打印出相应的报错信息,方便我们排查问题。

reportinfo#

通过reportinfo方法重写我们传入的name信息,就是我们在yaml文件中的测试用例名称信息。

这个就是我们通过对YamlTest的改造,组成了一个测试过程。这个类的核心是对requests的二次封装类。

二次封装requests#

我们打开common/request.py,我们键入以下内容。

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
requests二次封装
"""
import urllib3
from requests import Session, Response
from common.cache import cache
from utils.logger import loggerurllib3.disable_warnings()class HttpRequest(Session):"""requests方法二次封装"""def __init__(self, *args: t.Union[t.Set, t.List], **kwargs: t.Dict[t.Text, t.Any]):super(HttpRequest, self).__init__()self.exception = kwargs.get("exception", Exception)def send_request(self, **kwargs: t.Dict[t.Text, t.Any]) -> Response:"""发送请求"""try:logger.info("request data: {}".format(kwargs))method = kwargs.get('method', 'GET').upper()url = cache.get('baseurl') + kwargs.get('route')logger.info("Request Url: {}".format(url))logger.info("Request Method: {}".format(method))logger.info("Request Data: {}".format(kwargs))request_data = HttpRequest.mergedict(kwargs.get('RequestData'),headers=cache.get('headers'),timeout=cache.get('timeout'))response = self.dispatch(method, url, **request_data)logger.info("Request Result: {}{}".format(response, response.text))return responseexcept self.exception as e:logger.exception(format(e))raise edef dispatch(self, method, *args, **kwargs):"""请求分发"""handler = getattr(self, method.lower())return handler(*args, **kwargs)@staticmethoddef mergedict(args, **kwargs):"""合并字典"""for k, v in args.items():if k in kwargs:kwargs[k] = {**args[k], **kwargs.pop(k)}args.update(kwargs)return args

我们通过继承requests库的Session类,添加我们的定制化的一些方法。

send_request方法#

我们把YamlTest类中的测试用例数据传入到我们的这个方法中来,然后打印日志记录,并将结果进行返回。

dispatch#

在这个方法中我们根据传入的用例请求方法,去反射我们Session类中的相应的请求方法,从而实现get,post等HTTP请求。

mergedict#

编写了一个合并字典的方法,用来合并我们定义的请求体或者请求参数,和我们自定义的一些测试配置,比如headers,timeout等。

对于requests的封装暂时就介绍到这里。

处理response#

我们已经基本完成了测试框架的前半部分工作,剩下的章节基本都是后半部分内容了。这个章节我们来说一下我们获取到请求的结果之后怎么处理response(响应)。本来计划序列化和反序列化放一个章节的,但是鉴于内容有点少,所以我把4 5章节合并了。

序列化和反序列化#

我们打开common/json.py文件。

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
序列化和反序列化类
"""
import jsondef loads(content):"""反序列化json对象 -> python数据类型"""return json.loads(content)def dumps(content, ensure_ascii=True):"""序列化python数据类型 -> json对象"""return json.dumps(content, ensure_ascii=ensure_ascii)def is_json_str(string):"""验证是否为json字符串"""try:json.loads(string)return Trueexcept:return False

我们通过自带的json模块,封装两个方法

  • loads,这个主要用来把json字符串转换为python对象。
  • dumps,主要用来把python对象转换成json格式。
  • is_json_str我们可能需要对一个字符串是不是json格式需要做验证,所以我们写一个这样的方法。
正则处理#

在开始对response进行处理之前,我们需要封装一下正则方法。

打开common/regular文件,输入以下内容。

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
正则相关操作类
"""
import re
from common.json import is_json_str
from utils.logger import loggerdef get_var(key, raw_str):"""获取变量"""if is_json_str(raw_str):return re.compile(r'\"%s":"(.*?)"' % key).findall(raw_str)[0]return re.compile(r'%s' % key).findall(raw_str)[0]

这个的目的就是为了我们能在json数据中,通过名称能够获取到名称所对应的值。例如有以下字符串A

{"username":"admin"}

我们能够通过get_var(username, A),获取到admin的信息。

处理result#

当我们把准备工作做好之后我们就可以在result.py。文件中对我们的内容进行处理了。

我们打开common/result.py,输入以下内容:

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
response响应处理
"""
import re
import pytest
from common.cache import cache
from common.regular import re, get_var
from utils.logger import loggerdef check_results(r, validate):"""检查运行结果"""expectcode = validate.get('expectcode')resultcheck = validate.get('resultcheck')regularcheck = validate.get('regularcheck')if expectcode:pytest.assume(expectcode == r.status_code)if resultcheck:pytest.assume(resultcheck in r.text)if regularcheck:pytest.assume(re.findall(regularcheck, r.text))

可以看到我封装了检查运行结果的函数,这个里里面我用了一个类库。pytest-assume用过的朋友应该知道这个有什么作用。

官方地址:https://github.com/astraw38/pytest-assume

该插件的主要作用是,在断言失败后继续运行,并且会统计断言的报错情况。能够保证完整的运行,不会因为一个错误而发生整个测试停止的问题。

这个添加好之后我们,接着打开conftest.py文件,在YamlTest类中把我们这个方法集成进去。

from common.result import check_results+++def runtest(self):"""Some custom test execution (dumb example follows)."""r = self.request.send_request(**self.spec)self.response_handle(r, self.spec.get('Validate'))def response_handle(self, r, validate):"""Handling of responses"""if validate:check_results(r, validate)+++

我们在文件中添加以上内容。我们先创建一个response_handle处理方法。然后在runtest执行的时候导入这个方法,通过传入,请求的返回和需要验证的结果,通过check_result方法,我们基本就达到了简单的返回验证。

当然了我们这个只是最简单的,可能还有一些更复杂的,比如对数据的格式验证,和数据的返回层级验证,与数据库中的数据进行对比等验证操作。但是我这个只是一个简单的测试框架,还没有那么重,只是提供一种思路,剩下的实现就要靠你自己了,加油。

接口上下文关联#

前面我们已经完成了测试框架的主要功能了,读取用例,执行用例,获取结果。在这个请求中间呢,我们没有解决一个接口测试中很常见的问题,接口上下文参数传递,这个是什么意思呢。

比如我们可以用登录和登录验证这两个接口来讲一下,现在常用的系统都是前后端分离的,认证也是通过JWT的方式来搞定的,那么在登录接口进行登录之后就会生成一个token,我们拿到这个token就可以去其他接口进行鉴权,然后才能得到登录验证接口返回值。

所以我们这一章就解决一下这个请求参数上下文传递。

获取token#

先梳理一下思路,我们第一个请求的接口是登录接口,它会给我们返回token值,然后传到下一个接口中。所以我们按照执行顺序,先解决拿到返回值这一步。

在yaml文件中我们定义了一个字段Extract,这个字段就是预设一下我们要拿到哪一个值,你得告诉你的程序要那个他才能执行,在这个项目中我们想拿到的就是data这个。

  test_login:description: "登录"method: postroute: /weakPwdLogin/?from=web_loginRequestData:data:loginName: 18291900215password: dd636482aca022code:description: encryptValidate:expectcode: 200resultcheck: '"result":"success"'regularcheck: '[\d]{16}'Extract:   ---> 注意这一行- data

然后我们继续打开common/result.py这个文件,创建一个函数get_result,获取一下请求值。

def get_result(r, extract):"""获取值"""for key in extract:value = get_var(key, r.text)logger.debug("正则提取结果值:{}={}".format(key, value))cache.set(key, value)pytest.assume(key in cache)

这个函数的主要工作就是,通过正则表达式获取到结果,然后把他放入到缓存中去。

更新response_handle#

创建好之后,我们就需要去我们处理请求得地方把这个函数,给他嵌套进去。

打开conftest.py文件。

from common.result import get_result, check_results+++def response_handle(self, r: Response, validate: t.Dict, extract: t.List):"""Handling of responses"""if validate:check_results(r, validate)if extract:get_result(r, extract)

好了到这一步,我们的获取token(data)的工作就完成了。

接下来我们要处理的是传入到下一个接口中。

打开YAML测试文件,我们找到测试验证这条用例。我们会发现有一个${data},这是我们定义的一种变量格式。通过识别变量名称,去替换相应的结果。

  test_login_verify:description: "验证登录"method: postroute: /loginSuccess/RequestData:data:userId: "${data}"   ---> 这行Validate:expectcode: 200regularcheck:resultcheck: '"result":"success"'
进行替换#

我们首先得封装两个方法,一个方法让我们可以获取到这个用例里面有哪些我们需要替换的变量,一个方法可以让我们执行这个替换的过程。

打开common/regular.py.

from string import Template
from common.cache import cache+++def findalls(string):"""查找所有"""key = re.compile(r"\${(.*?)\}").findall(string)res = {k: cache.get(k) for k in key}logger.debug("需要替换的变量:{}".format(res))return resdef sub_var(keys, string):"""替换变量"""s = Template(string)res = s.safe_substitute(keys)logger.debug("替换结果:{}".format(res))return res
  • findalls

    我们通过正则去查找这个用例下有那些变量需要我们去替换。同时把需要替换的变量和变量值,以字典的形式进行存储。

  • sub_var

    通过python官方的string模块中的Template方法,我们可以轻松完成替换,因为我们的变量格式和该模块中的保持了一致。

编写好之后,我们打开common/request.py模块。

from common.json import json, loads, dumps+++class HttpRequest(Session):"""requests方法二次封装"""def __init__(self, *args, **kwargs):super(HttpRequest, self).__init__()self.exception = kwargs.get("exception", Exception)def send_request(self, **kwargs):try:+++logger.info("Request Url: {}".format(url))logger.info("Request Method: {}".format(method))kwargs_str = dumps(kwargs)if is_sub := findalls(kwargs_str):kwargs = loads(sub_var(is_sub, kwargs_str))logger.info("Request Data: {}".format(kwargs))request_data = HttpRequest.mergedict(kwargs.get('RequestData'),headers=cache.get('headers'),timeout=cache.get('timeout'))++++++

我们对send_request方法进行改造,在这里我们就用到了我们上一章编写的序列化和反序列化方法。

我们先把请求的dict数据,通过反序列化转换为json字符串。传给findalls方法获取到我们需要替换的变量。然后在调用我们编写的sub_var进行字符串的模板替换,生成新的json字符串,然后在通过序列化方法转换为dict数据,传给requests进行请求,这样我们就实现了,接口的上下文参数传递。是不是非常简单呢。

在完成以上操作后我们可以执行一下看看。

(env) > pytest
================================================================= test session starts =================================================================
platform win32 -- Python 3.8.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0   
rootdir: D:\VScode\Interface_test_example, configfile: pytest.ini       
plugins: assume-2.4.3, html-3.1.1, metadata-1.11.0
collecting ... 
----------------------------------------------------------------- live log collection ----------------------------------------------------------------- 
DEBUG 22:33:59 [regular.py:19] 11052 需要替换的变量:{}
DEBUG 22:33:59 [regular.py:27] 11052 替换结果:{"baseurl": "https://www.zhixue.com", "timeout": 30.0, "headers": {"Accept": "application/json, text/javascript, */*; q=0.01", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", "cookies": "aliyungf_tc=AQAAANNdlkvZ2QYAIb2Q221oiyiSOfhg; tlsysSessionId=cf0a8657-4a02-4a40-8530-ca54889da838; isJump=true; deviceId=27763EA6-04F9-4269-A2D5-59BA0FB1F154; 6e12c6a9-fda1-41b3-82ec-cc496762788d=webim-visitor-69CJM3RYGHMP79F7XV2M; loginUserName=18291900215", "X-Requested-With": "XMLHttpRequest"}}
collected 2 items                                                                                                                                       tests/testcase.yaml::\u767b\u5f55
-------------------------------------------------------------------- live log call -------------------------------------------------------------------- 
INFO 22:33:59 [request.py:51] 11052 request data: {'description': '登录', 'method': 'post', 'route': '/weakPwdLogin/?from=web_login', 'RequestData': {'data': {'loginName': 18291900215, 'password': 'dd636482aca022', 'code': None, 'description': 'encrypt'}}, 'Validate': {'expectcode': 200, 'resultcheck': 
'"result":"success"', 'regularcheck': '[\\d]{16}'}, 'Extract': ['data']}
INFO 22:33:59 [request.py:54] 11052 Request Url: https://www.zhixue.com/weakPwdLogin/?from=web_login
INFO 22:33:59 [request.py:55] 11052 Request Method: POST
DEBUG 22:33:59 [regular.py:19] 11052 需要替换的变量:{}
INFO 22:33:59 [request.py:59] 11052 Request Data: {'description': '登录', 'method': 'post', 'route': '/weakPwdLogin/?from=web_login', 'RequestData': {'data': {'loginName': 18291900215, 'password': 'dd636482aca022', 'code': None, 'description': 'encrypt'}}, 'Validate': {'expectcode': 200, 'resultcheck': 
'"result":"success"', 'regularcheck': '[\\d]{16}'}, 'Extract': ['data']}
INFO 22:34:00 [request.py:73] 11052 Request Result: <Response [200]>{"data":"1500000100070008427","result":"success"}
DEBUG 22:34:01 [result.py:21] 11052 正则提取结果值:data=1500000100070008427                                                                                                      
INFO 22:34:01 [request.py:51] 11052 request data: {'description': '验证登录', 'method': 'post', 'route': '/loginSuccess/', 'RequestData': {'data': {'userId': '${data}'}}, 'Validate': {'expectcode': 200, 'regularcheck': None, 'resultcheck': '"result":"success"'}}
INFO 22:34:01 [request.py:54] 11052 Request Url: https://www.zhixue.com/loginSuccess/
INFO 22:34:01 [request.py:55] 11052 Request Method: POST
DEBUG 22:34:01 [regular.py:19] 11052 需要替换的变量:{'data': '1500000100070008427'}
DEBUG 22:34:01 [regular.py:27] 11052 替换结果:{"description": "\u9a8c\u8bc1\u767b\u5f55", "method": "post", "route": "/loginSuccess/", "RequestData": {"data": {"userId": "1500000100070008427"}}, "Validate": {"expectcode": 200, "regularcheck": null, "resultcheck": "\"result\":\"success\""}}
INFO 22:34:01 [request.py:59] 11052 Request Data: {'description': '验证登录', 'method': 'post', 'route': '/loginSuccess/', 'RequestData': {'data': {'userId': '1500000100070008427'}}, 'Validate': {'expectcode': 200, 'regularcheck': None, 'resultcheck': '"result":"success"'}}
INFO 22:34:01 [request.py:73] 11052 Request Result: <Response [200]>{"result":"success"}
PASSED                                                                                                                                           [100%] 

可以看到执行成功了,经历了这么多我们才算创建了一个简单的接口自动化测试框架。

allure配置#

allure的安装这里就不在说了,之前有一篇文章是已经说过了,
pytest使用allure

配置allure信息#

安装好之后,我们先打开common/request.py文件,在里面做一下修改。

import allure+++def send_request(self, **kwargs: t.Dict[t.Text, t.Any]) -> Response:response = self.dispatch(method, url, **request_data)description_html = f"""<font color=red>请求方法:</font>{method}<br/><font color=red>请求地址:</font>{url}<br/><font color=red>请求头:</font>{str(response.headers)}<br/><font color=red>请求参数:</font>{json.dumps(kwargs, ensure_ascii=False)}<br/><font color=red>响应状态码:</font>{str(response.status_code)}<br/><font color=red>响应时间:</font>{str(response.elapsed.total_seconds())}<br/>"""allure.dynamic.description_html(description_html)logger.info("Request Result: {}{}".format(response, response.text))return response

在执行请求的时候我们记录一下,该次请求的详情信息。
接着我们打开,common/result.py,更新一下处理结果文件的代码。

import allure+++def get_result(r, extract):"""获取值"""for key in extract:value = get_var(key, r.text)logger.debug("正则提取结果值:{}={}".format(key, value))cache.set(key, value)pytest.assume(key in cache)with allure.step("提取返回结果中的值"):for key in extract:allure.attach(name="提取%s" % key, body=cache.get(key))def check_results(r, validate):"""检查运行结果"""expectcode = validate.get('expectcode')resultcheck = validate.get('resultcheck')regularcheck = validate.get('regularcheck')if expectcode:with allure.step("校验返回响应码"):allure.attach(name='预期响应码', body=str(expectcode))allure.attach(name='实际响应码', body=str(r.status_code))pytest.assume(expectcode == r.status_code)if resultcheck:with allure.step("校验响应预期值"):allure.attach(name='预期值', body=str(resultcheck))allure.attach(name='实际值', body=r.text)pytest.assume(resultcheck in r.text)if regularcheck:with allure.step("正则校验返回结果"):allure.attach(name='预期正则', body=regularcheck)allure.attach(name='响应值', body=str(re.findall(regularcheck, r.text)))pytest.assume(re.findall(regularcheck, r.text))

把上面这些工作加好之后,我们在命令行运行一下,带allure报告的cmd

pytest --html=report.html --self-contained-html --alluredir allure-results --clean-alluredir
allure generate allure-results -c -o allure-report
allure open allure-report

查看运行结果:

image

image

可以看到我们成功的把allure报告集成进来了,是不是很简单又很方便。

Jenkins集成#

安装Jenkins我就不在详细介绍了。

我们直接创建一个任务。

image

配置git仓库地址

image

增加构建时的运行命令

image

构建后的操作

image

我们执行一下:

image

发送邮件#

发送邮件之前已经说过了,将不再赘述。

原文链接:Jenkins构建失败发送邮件 - 随风挥手 - 博客园

开源地址#

由于智学网进行了接口调整,所以目前测试用例只有一个了。

到了这里我们的简易版的接口自动化测试框架已经全部完成了。为了方便学习交流,我们此项目的源代码放在了码云

https://gitee.com/wxhou/interface_test_example

欢迎各位在此交流学习,并留下你的想法。

高薪必备!18K接口自动化测试框架落地全流程|零基础到实战通关

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/93729.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/93729.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode每日一题,2025-8-20

统计全为1的正方形子矩阵 这题是正方形&#xff0c;比较简单 暴力就是二维前缀和。 或者用dp&#xff0c;dp[i][j]表示以i&#xff0c;j为右下角的最大正方形的边长&#xff0c;它由(i-1,j),(i,j-1),(i-1,j-1)三者共同决定&#xff0c;通过找规律可知&#xff0c;由三个的最小值…

在Excel启动时直接打开多个Excel文件

如果我们这段时间每天都要打开几个相同的Excel文件开展工作&#xff0c;可以把这几个文件放到一个文件夹&#xff08;如果原来就在不同的文件夹&#xff0c;就把快捷方式放到同一个文件夹&#xff09;&#xff0c;然后在Excel选项中设置启动时打开这个文件夹中的文件即可。注&a…

对象存储 COS 端到端质量系列 —— 终端网络诊断工具

背景 在COS终端SDK的众多功能中&#xff0c;文件上传功能的使用颇为频繁。鉴于此&#xff0c;提升文件上传的成功率便显得至关重要。众多导致上传失败的因素里&#xff0c;由网络问题引发的故障无疑是其中影响上传成功率的关键因素之一&#xff0c;同时也是最难以把控的要素。…

Flask高效数据库操作指南

Flask-SQLAlchemy 数据库操作关于Flask数据库Flask中没有指定使用的数据库&#xff0c;可以自由选择不管你是使用关系型数据库&#xff0c;还是非关系型数据库都可以&#xff0c;不像django提供了orm 数据库抽象层&#xff0c;可以直接采用对象的方式操作数据库。但是为了开发效…

Camera相机人脸识别系列专题分析之十九:MTK ISP6S平台FDNode原生代码

【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、问题背景 二、MTK ISP6S平台FDNodeImp.cpp 2.1:原生代码

S32K3 的图形化配置和EB配置mcal差异

32K3系列的图形化配置工具&#xff08;如S32 Design Studio, S32DS&#xff09;与EB配置工具&#xff08;基于EB tresos Studio&#xff09;在配置MCAL&#xff08;Microcontroller Abstraction Layer&#xff09;时存在关键差异&#xff0c;主要体现在工具环境、配置流程、代码…

Meta 再次重组人工智能部门

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

在nodejs中 有哪些是 “假值“和怎么写 “双重否定”

1.在 Node.js&#xff08;以及 JavaScript&#xff09;中&#xff0c;以下值在布尔上下文&#xff08;例如 if 语句、while 循环条件等&#xff09;中被视为 “假值”&#xff1a;false&#xff1a;布尔类型的 false 值&#xff0c;这是最直接的假值。if (false) {console.log(…

车载软件架构 --- 赢得汽车软件开发竞赛

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

机器学习总复习

这段时间学习了 KNN&#xff0c;线性回归&#xff0c;逻辑回归&#xff0c;贝叶斯&#xff0c;聚类&#xff08;K-means,DBSCAN&#xff09;&#xff0c;决策树&#xff0c;集成学习&#xff08;随机森林&#xff0c;XGboost&#xff09;&#xff0c;SVM支持向量机&#xff0c;…

深入解析EventPoller:Disruptor的轮询式事件处理机制

EventPoller 是什么&#xff1f;EventPoller 是 Disruptor 框架中一种 基于轮询&#xff08;poll-based&#xff09; 的事件消费机制。它与我们更常见的 BatchEventProcessor&#xff08;基于独立的消费者线程&#xff09;形成了对比。核心区别在于&#xff1a;BatchEventProce…

K8S-Secret资源对象

目录 一、Secret概述 二、Secret 类型 kubectl 创建类型 三、Secret 使用 Opaque 类型 Secret 的使用 创建 yaml 一、Secret概述 k8s secrets用于存储和管理一些敏感数据&#xff0c;比如密码&#xff0c;token&#xff0c;密钥等敏感信息。它把 Pod 想要访问的加密数据…

lua入门以及在Redis中的应用

1.基本语法1.1变量lua的变量有&#xff1a;无效值nil&#xff0c;布尔值boolean&#xff0c;数字number、字符串string、函数function、自定义类型userdata、线程thread、表table&#xff08;key-value结构&#xff09;1.2循环数值循环for i起始值, 结束值 ,间隔值 do---option…

淘宝电商大数据采集【采集内容||采集方法|工具||合规性||应用】

淘宝电商大数据采集是指通过技术手段、工具或平台&#xff0c;系统性收集淘宝&#xff08;及旗下天猫等&#xff09;生态内的各类数据&#xff0c;用于分析市场趋势、用户行为、商品表现、竞品动态等&#xff0c;为电商运营、决策提供数据支持。以下从采集内容、工具方法、合规…

ROS2核心模块

1.创建工作空间先创建工作空间ws01_plumbing&#xff0c;终端下进入工作空间的src目录&#xff0c;执行如下命令&#xff1a;ros2 pkg create --build-type ament_cmake base_interfaces_demo2.话题通信话题通信是ROS中使用频率最高的一种通信模式&#xff0c;话题通信是基于发…

Mac 上安装并使用 frpc(FRP 内网穿透客户端)指南

一、先装好 Homebrew&#xff08;macOS 的包管理器&#xff09;打开终端&#xff08;Terminal&#xff09;&#xff0c;先装命令行开发工具 xcode-select --install弹窗点“安装”&#xff0c;等待 3~5 分钟。一键安装 Homebrew /bin/bash -c "$(curl -fsSL https://raw.g…

04_接口与包管理

第4课:接口与包管理 课程目标 深入理解Go语言接口的概念和用法 掌握接口的组合和空接口 学会使用Go Modules进行包管理 理解包的导入和导出规则 1. 接口基础 1.1 接口定义 // 基本接口定义 type Shape interface {Area() float64Perimeter()

福昕PDF编辑软件高级版下载与详细图文安装教程!!

软件下载 【软件名称】&#xff1a; 福昕PDF编辑器高级版 【软件大小】&#xff1a;668.9MBa a【系统要求】&#xff1a;awin10/win11或更高 福昕&#xff0c;软件下载&#xff08;夸克网盘需手机打开&#xff09;&#xff1a;&#xff1a;福昕丨夸克网盘-资源免费下载 软件介…

利用无事务方式插入数据库解决并发插入问题(最小主键id思路)

一、背景 由于某业务需要回退某产品数据缓存列表Asset资源&#xff0c;主任务执行后&#xff0c;通过并行执行批量子任务进行数据回退&#xff0c;子任务中会记录缓存列表Asset和缓存列表行AssetLine数据&#xff0c;并行执行过程会出现缓存列表行AssetLine重复插入问题&#…

如何制作免费的比特币冷钱包

本文主要从技术上讨论冷钱包的操作机制和原理&#xff0c;并不作为投资建议。对于国外的比特币玩家&#xff0c;或者打算长期囤币来对抗通货膨胀的&#xff0c;或者是想短期持有的&#xff0c;那么将比特币存储在哪里是一个Common的问题。一般是两类选择。第一种选择是存储在交…