tep关键字驱动框架教程

【原创】爆肝23页教程,自研关键字驱动框架_用例

tep简介

tepTry Easy Pytest的首字母缩写,关键字驱动框架,专注于接口自动化测试,单个文件即可完成用例编写。

设计理念

✔️稳定:基于成熟框架pytest,天生强大

✔️规范:RobotFramework风格,井井有条

✔️统一:关键字命名与JMeter组件一致,一知万用

✔️原生:关键字用法保留Python原生定义,轻车熟路

✔️兼容:分层机制保证迭代升级不影响老项目,向下兼容

❌拒绝低代码平台,开发成本太高。

❌拒绝EXCEL/YAML,调试太麻烦。

❌拒绝深度编程,绕来绕去太复杂。

✌️只需要一点点Python基础,就能轻松搞定接口自动化。

快速入门

安装

pip install tep

验证安装成功:

tep -v
Current Version: V2.0.0

 ____o__ __o____   o__ __o__/_   o__ __o
  /   \   /   \   <|    v       <|     v\
       \o/        < >           / \     <\
        |          |            \o/     o/
       < >         o__/_         |__  _<|/
        |          |             |
        o         <o>           <o>
       <|          |             |
       / \        / \  _\o__/_  / \

新建项目

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请求。

print("Hello, Python!")

Python变量

简单理解,= 符号左边的就是变量,变量用来存储数据。

Python数据类型

Number(数字)

x = 123

String(字符串)

s = "cekaigang"

List(列表)

skills = ["测试", "开发"]
# 索引取值
skills[0]

Tuple(元组)

也就是不可变列表。

tup3 = (1, 2, 3)
# 索引取值
tup3[0]

Set(集合)

# 用于求交集、并集等
sites = {'Google', 'Taobao', 'Runoob', 'Facebook', 'Zhihu', 'Baidu'}

Dictionary(字典)

c = {"x": 1, "y": 2}
# 中括号key取值
c["x"]

Python缩进

Python语言特点就是使用4个空格来控制代码块。

def hello() :
    print("Hello World!")

Python函数

函数定义:

def 函数名(参数列表):
    函数体

tep用例就是写在一个test()函数里面的。

函数调用:

# 使用小括号来调用
UserDefinedVariablesKeyword()
# 将函数返回值存入变量
ro = UserDefinedVariablesKeyword()
# 给函数传参,参数可以只传值,也可以传键值对key=value
ro = HTTPRequestKeyword("get", url="http://httpbin.org/status/200")

Python对象

对象包含字段和方法,使用.符合来访问。

# 字段
response.status_code
# 方法
response.json()

Python导入

从其他文件导入代码到当前文件使用。

from tep.utils.Parewise import pairwise

没错,就是这么简单,掌握这些基础语法就能开始使用tep框架了。

框架语法

tep命令

tep -v,等同于tep --version,查看版本

tep -s,等同于tep --startproject,新建项目

关键字语法

关键字不需要import就能使用,将关键字传入test()函数即可。

关键字以Keyword单词结尾,在输入K时能获得语法提示:

【原创】爆肝23页教程,自研关键字驱动框架_自定义_02

def test(Keyword):
    # 返回结果 = 关键字(参数)
    ro = Keyword(param)

关键字跟Python函数用法一样,接受传入参数,执行某个动作,返回操作结果。

关键字概览

框架内置关键字

  • HTTPRequestKeyword 发送HTTP请求
  • BodyKeyword 对接口参数进行参数化
  • UserDefinedVariablesKeyword 用户自定义变量
  • DataKeyword 读取data目录下文件
  • DbcKeyword 数据库连接

用户自定义关键字

  • login 登录
  • mysql_execute 执行sql

更多关键字内容请阅读“高级用法>关键字详解”章节内容。

代码规范

  1. 一条语句写在一行,不换行,超长时建议通过变量拆成多条语句
  2. 关键字返回Result对象,可命名为ro,通过ro.取值
  3. 遵守PEP8,祝您写出漂亮代码

目录结构

【原创】爆肝23页教程,自研关键字驱动框架_自定义_03

  • case 存放用例文件
  • data 存放数据文件
  • report 存放报告文件
  • run.py 执行用例入口

用例管理

  1. 用例全部写在一个文件里面,从上往下分成多个段落,每个段落视为一个测试步骤。用例由多个测试步骤组成。
  2. 测试步骤分为①前置数据准备②接口请求③后置数据提取三大部分。步骤由关键字驱动。
  3. 多条用例按不同模块放在不同目录下,由于用例文件完全独立,可以将稳定用例全部放到某个目录下,命名为“基础用例集”,进行持续维护和定时巡检,执行时指定目录即可。

以上是作者建议,用例管理是很灵活的,框架没有做任何限制,可以自由选择。

用例执行有3种主要方式:

  1. 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)
  1. PyCharm执行
  2. 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语句:

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关键字:

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框架核心,语法统一:

ro = Keyword(param)

任何关键字都遵循这种用法。

tep关键字分为内置和自定义两大类。

内置

内置关键字命名为单词首字母大写且以Keyword结尾。

HTTPRequestKeyword

学习requests.request即可,HTTPRequestKeyword使用方法完全一样。

ro = HTTPRequestKeyword("get", url="http://httpbin.org/status/200")

HTTPRequestKeyword关键字返回Result对象,通过ro.response获取requests.Reponse对象。

BodyKeyword

第一个参数为JSON字符串,注意使用多行字符串且加上前缀r

第二个参数为表达式,key为JSONPath表达式,value为替换值,支持批量替换。

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

不需要传参,直接使用。

ro = UserDefinedVariablesKeyword()

UserDefinedVariablesKeyword关键字返回Result对象,通过ro.data获取解析后字典。

DataKeyword

入参为文件路径,data目录相对路径。

ro = DataKeyword("data.json")

DataKeyword关键字返回Result对象,通过ro.data获取解析后字典。

自定义

自定义关键字命名为小写加下划线。需要用户输入数据的关键字为自定义关键字,比如登录信息、数据库连接信息。

自定义关键字需要新建fixture文件夹,文件名以fixture_开头才能识别:

【原创】爆肝23页教程,自研关键字驱动框架_用例_04

login

使用:

def test(login):
    ro = login()
    print(ro.data)

定义:

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

使用:

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

定义:

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装饰器即可定义。

为了规范和统一,建议采用以下原则:

  1. 自定义关键字使用小写字母加下划线命名,跟tep内置关键字区分
  2. 定义一个内部函数,返回函数名
  3. 内部函数返回Result对象

tep框架除了conftest.py定义的fixture,也能识别fixture目录下以fixture_开头的文件中,定义的fixture,并自动加载,建议把自定义关键字都放在fixture目录下。

基本结构:

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用户可以创建虚拟环境并激活:

python3 -m venv venv
source venv/bin/activate

创建项目时,带上-venv参数,可创建单个项目的Python虚拟环境,并在该项目的虚拟环境中安装tep:

tep -s demo -venv

三方库

tep用到了很多三方库,可以学习和使用,以更好使用框架:

pytest、requests、jsonpath、pymysql、pytest-xdist、loguru、faker等。

实用案例

场景用例

登录,搜索商家,添加购物车,下单,支付:

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代码格式化

快捷键:

【原创】爆肝23页教程,自研关键字驱动框架_Python_05

PyCharm格式化代码不换行

默认120字符换行,根据显示屏宽度调整:

【原创】爆肝23页教程,自研关键字驱动框架_用例_06

typing语法提示

给变量通过: Type指定类型后,在使用时输入.就能被PyCharm识别从而获得语法提示:

【原创】爆肝23页教程,自研关键字驱动框架_用例_07

版本升级

pip install -U tep

tep做了向下兼容,请放心升级,如果升级后出现不兼容问题,请联系作者。

更新日志

V2.0.0 tep关键字驱动框架

V1.0.0 tep小工具完整教程

V0.2.3 tep小工具首次开源

源码地址

如果对您有所帮助,请帮忙给开源项目点个Star吧,感谢您的支持!

https://github.com/dongfanger/tep

在线文档

飞书文档,可评论:

https://eqgvpqzl6c.feishu.cn/docx/DZVed7YptocKE1xYIgici1DynTe

答疑解惑

  • 怎么向其他人介绍tep框架?
    我发现了一个框架,关键字驱动的,只在一个文件里面就能把一条接口自动化用例写完。
  • 不懂代码能使用tep框架吗?
    不能。学嘛,简单入门就能用,Python这么流行,学起来。
  • conftest.py无法识别?
    pytest7.4.0版本更新,默认只有在conftest.py相同目录执行pytest命令才能识别,如果是在子目录执行pytest则无法识别,要么显示指定--confcutdir目录位置到conftest.py所在目录,要么添加空的pytest.ini配置文件。

【原创】爆肝23页教程,自研关键字驱动框架_用例_08

  • 向下兼容的分层机制怎么做的?
    tep.keywords.api做了一个适配层,暴露给用户的入参为*args, **kwargs,出参为Result对象,确保后续升级无论怎么变动入参和出参,对老项目是无感知的。

【原创】爆肝23页教程,自研关键字驱动框架_自定义_09

联系作者

公众号《测试开发刚哥》

【原创】爆肝23页教程,自研关键字驱动框架_Python_10


公众号【测试开发刚哥】