tep关键字驱动框架教程
tep简介
tep
是Try Easy Pytest的首字母缩写,关键字驱动框架,专注于接口自动化测试,单个文件即可完成用例编写。
设计理念
✔️稳定:基于成熟框架pytest,天生强大
✔️规范:RobotFramework风格,井井有条
✔️统一:关键字命名与JMeter组件一致,一知万用
✔️原生:关键字用法保留Python原生定义,轻车熟路
✔️兼容:分层机制保证迭代升级不影响老项目,向下兼容
❌拒绝低代码平台,开发成本太高。
❌拒绝EXCEL/YAML,调试太麻烦。
❌拒绝深度编程,绕来绕去太复杂。
✌️只需要一点点Python基础,就能轻松搞定接口自动化。
快速入门
安装
代码语言:javascript复制pip install tep
验证安装成功:
代码语言:javascript复制tep -v
Current Version: V2.0.0
____o__ __o____ o__ __o__/_ o__ __o
/ / <| v <| v
o/ < > / <
| | o/ o/
< > o__/_ |__ _<|/
| | |
o <o> <o>
<| | |
/ / _o__/_ /
新建项目
代码语言:javascript复制tep -s demo
Created folder: demo
Created folder: demo/case
Created folder: demo/data
Created folder: demo/report
Created file: demo/run.py
Created file: demo/conftest.py
Created file: demo/pytest.ini
Created file: demo/.gitignore.py
Created file: demo/case/__init__.py
Created file: demo/case/test_demo.py
Created file: demo/data/UserDefinedVariables.yaml
编写用例
在case/test_demo.py
编写用例,脚手架已自动生成:
def test(HTTPRequestKeyword):
ro = HTTPRequestKeyword("get", url="http://httpbin.org/status/200")
assert ro.response.status_code == 200
执行run.py
后出现以下日志:
URL: http://httpbin.org/status/200
Method: GET
Headers: {"User-Agent": "python-requests/2.31.0", "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "Connection": "keep-alive"}
Request Body: None
Status Code: 200
Response Body:
Elapsed: 0.61046s
恭喜您,上手成功!
基础语法
tep框架是编写Python代码的,需要具备一些Python基础。不过无需担心,只要简单入门即可。
Python文件
Python文件是以.py
结尾的。可以使用python filename.py
命令执行,也可以在PyCharm中右键点击Run
按钮执行。
Python语句
一条语句完成一件事,比如打印日志、发送HTTP请求。
代码语言:javascript复制print("Hello, Python!")
Python变量
简单理解,=
符号左边的就是变量,变量用来存储数据。
Python数据类型
Number(数字)
代码语言:javascript复制x = 123
String(字符串)
代码语言:javascript复制s = "cekaigang"
List(列表)
代码语言:javascript复制skills = ["测试", "开发"]
# 索引取值
skills[0]
Tuple(元组)
也就是不可变列表。
代码语言:javascript复制tup3 = (1, 2, 3)
# 索引取值
tup3[0]
Set(集合)
代码语言:javascript复制# 用于求交集、并集等
sites = {'Google', 'Taobao', 'Runoob', 'Facebook', 'Zhihu', 'Baidu'}
Dictionary(字典)
代码语言:javascript复制c = {"x": 1, "y": 2}
# 中括号key取值
c["x"]
Python缩进
Python语言特点就是使用4个空格来控制代码块。
代码语言:javascript复制def hello() :
print("Hello World!")
Python函数
函数定义:
代码语言:javascript复制def 函数名(参数列表):
函数体
tep用例就是写在一个test()
函数里面的。
函数调用:
代码语言:javascript复制# 使用小括号来调用
UserDefinedVariablesKeyword()
# 将函数返回值存入变量
ro = UserDefinedVariablesKeyword()
# 给函数传参,参数可以只传值,也可以传键值对key=value
ro = HTTPRequestKeyword("get", url="http://httpbin.org/status/200")
Python对象
对象包含字段和方法,使用.
符合来访问。
# 字段
response.status_code
# 方法
response.json()
Python导入
从其他文件导入代码到当前文件使用。
代码语言:javascript复制from tep.utils.Parewise import pairwise
没错,就是这么简单,掌握这些基础语法就能开始使用tep框架了。
框架语法
tep命令
tep -v
,等同于tep --version
,查看版本
tep -s
,等同于tep --startproject
,新建项目
关键字语法
关键字不需要import就能使用,将关键字传入test()
函数即可。
关键字以Keyword
单词结尾,在输入K
时能获得语法提示:
def test(Keyword):
# 返回结果 = 关键字(参数)
ro = Keyword(param)
关键字跟Python函数用法一样,接受传入参数,执行某个动作,返回操作结果。
关键字概览
框架内置关键字
- HTTPRequestKeyword 发送HTTP请求
- BodyKeyword 对接口参数进行参数化
- UserDefinedVariablesKeyword 用户自定义变量
- DataKeyword 读取data目录下文件
- DbcKeyword 数据库连接
用户自定义关键字
- login 登录
- mysql_execute 执行sql
更多关键字内容请阅读“高级用法>关键字详解”章节内容。
代码规范
- 一条语句写在一行,不换行,超长时建议通过变量拆成多条语句
- 关键字返回Result对象,可命名为
ro
,通过ro.
取值 - 遵守PEP8,祝您写出漂亮代码
目录结构
- case 存放用例文件
- data 存放数据文件
- report 存放报告文件
- run.py 执行用例入口
用例管理
- 用例全部写在一个文件里面,从上往下分成多个段落,每个段落视为一个测试步骤。用例由多个测试步骤组成。
- 测试步骤分为①前置数据准备②接口请求③后置数据提取三大部分。步骤由关键字驱动。
- 多条用例按不同模块放在不同目录下,由于用例文件完全独立,可以将稳定用例全部放到某个目录下,命名为“基础用例集”,进行持续维护和定时巡检,执行时指定目录即可。
以上是作者建议,用例管理是很灵活的,框架没有做任何限制,可以自由选择。
用例执行有3种主要方式:
run.py
执行
from tep.libraries.Run import Run
if __name__ == '__main__':
settings = {
"path": ["test_demo.py"], # Path to run, relative path to case
"report": False, # Output test report or not
"report_type": "pytest-html" # "pytest-html" "allure"
}
Run(settings)
- PyCharm执行
- pytest命令执行
测试报告
支持2种测试报告,pytst-html和allure。在run.py
文件中设置。
默认为pytest-html,无需单独安装,开启后会生成HTML报告到report目录下。
allure需要安装Java环境,然后下载文件,解压后将bin目录添加到系统环境变量Path。
https://github.com/allure-framework/allure2/releases
开启allure报告前请确保已完成安装,否则可能报错找不到allure命令。
断言方法
直接使用Python原生断言,assert语句:
代码语言:javascript复制def test_assert_equal():
assert 1 == 1
def test_assert_not_equal():
assert 1 != 2
def test_assert_greater_than():
assert 2 > 1
def test_assert_less_than():
assert 1 < 2
def test_assert_less_or_equals():
assert 2 >= 1
assert 2 >= 2
def test_assert_greater_or_equals():
assert 1 <= 2
assert 1 <= 1
def test_assert_length_equal():
assert len("abc") == len("123")
def test_assert_length_greater_than():
assert len("hello") > len("123")
def test_assert_length_less_than():
assert len("hi") < len("123")
def test_assert_length_greater_or_equals():
assert len("hello") >= len("123")
assert len("123") >= len("123")
def test_assert_length_less_or_equals():
assert len("123") <= len("hello")
assert len("123") <= len("123")
def test_assert_string_equals():
assert "dongfanger" == "dongfanger"
def test_assert_startswith():
assert "dongfanger".startswith("don")
def test_assert_regex_match():
import re
assert re.findall(r"don.*er", "dongfanger")
def test_assert_contains():
assert "fang" in "dongfanger"
assert 2 in [2, 3]
assert "x" in {"x": "y"}.keys()
def test_assert_type_match():
assert isinstance(1, int)
assert isinstance(0.2, float)
assert isinstance(True, bool)
assert isinstance(3e 26j, complex)
assert isinstance("hi", str)
assert isinstance([1, 2], list)
assert isinstance((1, 2), tuple)
assert isinstance({"a", "b", "c"}, set)
assert isinstance({"x": 1}, dict)
变量管理
全局变量在data/UserDefinedVariables.yaml
中填写,通过UserDefinedVariablesKeyword()
关键字直接读取。
局部变量在用例文件中test()
函数内直接定义。
其他变量可以在data
目录下新建不同的YAML/JSON文件,通过DataKeyword
读取。
接口关联
接口关联是指从上个接口响应取值,将值传入下个接口入参,即参数化。
取值
TepResponse内置了.jsonpath()
方法:
sku_id = response.jsonpath("$.skuId")
默认取匹配到的第一个,更复杂取值使用JSONPath原生方法。
传值
使用BodyKeyword关键字:
代码语言:javascript复制body = r"""{"id":1,"param":"[{"page": 1, "pinList":["cekaigang"]}]","ext1":{"a":1,"b":1},"ext2":[1,1,1],"ext3":{"name":"pytest"}}"""
ro = BodyKeyword(body, {"$.id": 9, "$.param[0].page": 9, "$.param[0].pinList[0]": "dongfanger", "$.ext1.a": 9, "$.ext2[0]": 9, "$.ext2[2]": 9, "$.ext3.name": "tep"})
第一个参数为JSON字符串,注意使用多行字符串且加上前缀r
。
第二个参数为表达式,key为JSONPath表达式,value为替换值,支持批量替换。
接口复用
接口复用,或者叫做“用例复用”,通过自定义关键字来实现。可以将多个接口,或者公共用例,自定义为关键字,使用关键字在不同用例之间复用。
高级用法
关键字详解
关键字是tep框架核心,语法统一:
代码语言:javascript复制ro = Keyword(param)
任何关键字都遵循这种用法。
tep关键字分为内置和自定义两大类。
内置
内置关键字命名为单词首字母大写且以Keyword
结尾。
HTTPRequestKeyword
学习requests.request即可,HTTPRequestKeyword使用方法完全一样。
代码语言:javascript复制ro = HTTPRequestKeyword("get", url="http://httpbin.org/status/200")
HTTPRequestKeyword关键字返回Result对象,通过ro.response
获取requests.Reponse对象。
BodyKeyword
第一个参数为JSON字符串,注意使用多行字符串且加上前缀r
。
第二个参数为表达式,key为JSONPath表达式,value为替换值,支持批量替换。
代码语言:javascript复制ro = BodyKeyword(body, {"$.id": 9, "$.param[0].page": 9, "$.param[0].pinList[0]": "dongfanger", "$.ext1.a": 9, "$.ext2[0]": 9, "$.ext2[2]": 9, "$.ext3.name": "tep"})
BodyKeyword关键字返回Result对象,通过ro.data
获取替换后JSON。
UserDefinedVariablesKeyword
不需要传参,直接使用。
代码语言:javascript复制ro = UserDefinedVariablesKeyword()
UserDefinedVariablesKeyword关键字返回Result对象,通过ro.data
获取解析后字典。
DataKeyword
入参为文件路径,data目录相对路径。
代码语言:javascript复制ro = DataKeyword("data.json")
DataKeyword关键字返回Result对象,通过ro.data
获取解析后字典。
自定义
自定义关键字命名为小写加下划线。需要用户输入数据的关键字为自定义关键字,比如登录信息、数据库连接信息。
自定义关键字需要新建fixture文件夹,文件名以fixture_
开头才能识别:
login
使用:
代码语言:javascript复制def test(login):
ro = login()
print(ro.data)
定义:
代码语言:javascript复制import pytest
from tep.libraries.Result import Result
@pytest.fixture(scope="session")
def login(HTTPRequestKeyword):
def _function() -> Result:
url = "http://127.0.0.1:5000/login"
headers = {"Content-Type": "application/json"}
body = {"username": "dongfanger", "password": "123456"}
ro = HTTPRequestKeyword("post", url=url, headers=headers, json=body)
response = ro.response
assert response.status_code < 400
ro = Result()
ro.data = {"Content-Type": "application/json", "Cookie": f"{response.json()['Cookie']}"}
return ro
return _function
mysql_execute
使用:
代码语言:javascript复制def test(mysql_execute):
sql = "select 1 from dual"
ro = mysql_execute(sql)
cursor = ro.cursor
column_names = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
for row in rows:
print(row)
print(row[column_names.index("1")]) # get by column name
定义:
代码语言:javascript复制import pytest
from tep.libraries.DB import DB
from tep.libraries.Result import Result
@pytest.fixture(scope="session")
def mysql_execute(DbcKeyword):
ro = DbcKeyword(host="127.0.0.1", port=3306, user="root", password="12345678", database="sys")
conn = ro.conn
def _function(sql: str) -> Result:
cursor = conn.cursor()
DB.pymysql_execute(conn, cursor, sql)
ro = Result()
ro.cursor = cursor
return ro
yield _function
conn.close() # After test, close connection
自定义关键字
关键字本质上是pytest fixture,使用@pytest.fixture
装饰器即可定义。
为了规范和统一,建议采用以下原则:
- 自定义关键字使用小写字母加下划线命名,跟tep内置关键字区分
- 定义一个内部函数,返回函数名
- 内部函数返回Result对象
tep框架除了conftest.py定义的fixture,也能识别fixture
目录下以fixture_
开头的文件中,定义的fixture,并自动加载,建议把自定义关键字都放在fixture
目录下。
基本结构:
代码语言:javascript复制import pytest
from tep.libraries.Result import Result
@pytest.fixture(scope="session") # 固定
def keyword_name(other_keyword): # 关键字命名,可以引用其他关键字
def _function(param) -> Result: # 内部函数,定义参数
# 编写逻辑代码
ro = Result()
ro.data = "" # 将数据存入Result对象
return ro # 返回Result对象
return _function # 将内部函数返回,使用时就能像函数一样调用
创建虚拟环境
安装时,MAC用户可以创建虚拟环境并激活:
代码语言:javascript复制python3 -m venv venv
source venv/bin/activate
创建项目时,带上-venv
参数,可创建单个项目的Python虚拟环境,并在该项目的虚拟环境中安装tep:
tep -s demo -venv
三方库
tep用到了很多三方库,可以学习和使用,以更好使用框架:
pytest、requests、jsonpath、pymysql、pytest-xdist、loguru、faker等。
实用案例
场景用例
登录,搜索商家,添加购物车,下单,支付:
代码语言:javascript复制def test(HTTPRequestKeyword, BodyKeyword, login):
ro = login()
var = {"domain": "http://127.0.0.1:5000", "headers": ro.data}
url = var["domain"] "/searchSku" "?skuName=book"
ro = HTTPRequestKeyword("get", url=url, headers=var["headers"])
assert ro.response.status_code < 400
sku_id = ro.response.jsonpath("$.skuId")
sku_price = ro.response.jsonpath("$.price")
url = var["domain"] "/addCart"
body = r"""{"skuId":1,"skuNum":2}"""
ro = BodyKeyword(body, {"$.skuId": sku_id})
body = ro.data
ro = HTTPRequestKeyword("post", url=url, headers=var["headers"], json=body)
assert ro.response.status_code < 400
sku_num = ro.response.jsonpath("$.skuNum")
total_price = ro.response.jsonpath("$.totalPrice")
url = var["domain"] "/order"
body = r"""{"skuId":1,"price":2,"skuNum":3,"totalPrice":4}"""
ro = BodyKeyword(body, {"$.skuId": sku_id, "$.price": sku_price, "$.skuNum": sku_num, "$.totalPrice": total_price})
body = ro.data
ro = HTTPRequestKeyword("post", url=url, headers=var["headers"], json=body)
assert ro.response.status_code < 400
order_id = ro.response.jsonpath("$.orderId")
url = var["domain"] "/pay"
body = r"""{"orderId":1,"payAmount":"0.2"}"""
ro = BodyKeyword(body, {"$.orderId": order_id})
body = ro.data
ro = HTTPRequestKeyword("post", url=url, headers=var["headers"], json=body)
assert ro.response.status_code < 400
assert ro.response.jsonpath("$.success") == "true"
该用例很好的展示了一个文件写用例,按段落,分步骤,编写的思想。
启动mock服务,位于源码tests/scripts/mock.py
,可以运行此条用例成功。
仅登录一次
单进程串行:
login自定义关键字的scope="session"
表示整个测试阶段都只执行一次登录。共有这些维度:session、package、module、class、function,如果设置为function则表示每次函数都要登录,其他同理。
多进程并行:
通过pytest-xdist
可以实现多进程并行执行用例,为了保证全局只执行一次登录,可以自定义关键字login_xdist
:
import json
import pytest
from filelock import FileLock
from tep.libraries.Result import Result
@pytest.fixture(scope="session")
def login_xdist(HTTPRequestKeyword, tmp_path_factory, worker_id):
"""
Xdist is used in a distributed manner, and this login will only be executed globally once throughout the entire runtime
Reference: https://pytest-xdist.readthedocs.io/en/latest/how-to.html#making-session-scoped-fixtures-execute-only-once
"""
def _login():
url = "http://127.0.0.1:5000/login"
headers = {"Content-Type": "application/json"}
body = {"username": "dongfanger", "password": "123456"}
ro = HTTPRequestKeyword("post", url=url, headers=headers, json=body)
response = ro.response
assert response.status_code < 400
ro = Result()
ro.data = {"Content-Type": "application/json", "Cookie": f"{response.json()['Cookie']}"}
return ro
if worker_id == "master":
# not executing in with multiple workers, just produce the data and let
# pytest's fixture caching do its job
return _login
# get the temp directory shared by all workers
root_tmp_dir = tmp_path_factory.getbasetemp().parent
fn = root_tmp_dir / "data.json"
with FileLock(str(fn) ".lock"):
if fn.is_file():
_function = json.loads(fn.read_text())
else:
_function = _login
fn.write_text(json.dumps(_function))
return _function
实用技巧
Python代码格式化
快捷键:
PyCharm格式化代码不换行
默认120字符换行,根据显示屏宽度调整:
typing语法提示
给变量通过: Type
指定类型后,在使用时输入.
就能被PyCharm识别从而获得语法提示:
版本升级
代码语言:javascript复制pip install -U tep
tep做了向下兼容,请放心升级,如果升级后出现不兼容问题,请联系作者。
更新日志
V2.0.0 tep关键字驱动框架
V1.0.0 tep小工具完整教程
V0.2.3 tep小工具首次开源
源码地址
如果对您有所帮助,请帮忙给开源项目点个Star吧,感谢您的支持!
https://github.com/dongfanger/tep
答疑解惑
- 怎么向其他人介绍tep框架? 我发现了一个框架,关键字驱动的,只在一个文件里面就能把一条接口自动化用例写完。
- 不懂代码能使用tep框架吗? 不能。学嘛,简单入门就能用,Python这么流行,学起来。
- conftest.py无法识别?
pytest7.4.0版本更新,默认只有在conftest.py相同目录执行pytest命令才能识别,如果是在子目录执行pytest则无法识别,要么显示指定
--confcutdir
目录位置到conftest.py所在目录,要么添加空的pytest.ini
配置文件。
- 向下兼容的分层机制怎么做的?
在
tep.keywords.api
做了一个适配层,暴露给用户的入参为*args, **kwargs
,出参为Result
对象,确保后续升级无论怎么变动入参和出参,对老项目是无感知的。
参考资料: 《测试开发刚哥电子书》https://dongfanger.gitee.io/blog/ 电子书全部由刚哥原创,包含了大量技术文章,包含Python语言、pytest测试框架、teprunner测试平台等。