蒙珣的博客

活好当下,做好今天该做的事情。

0%

Pytest笔记

pytest用例管理框架(单元测试框架)

主要作用

  • 发现测试用例:从多个py文件中按照一定的规则找到测试用例
  • 执行测试用例:按照一定的顺序执行测试用例,生成测试结果
    • pytest:默认从上到下,可以使用装饰器改变规则
    • unittest:默认按照ASCII顺序执行
  • 判断测试结果:断言
  • 生成测试报告:pytest-html,allure报告

pytest简介

  1. 基于python的单元测试框架,他可以和selenium、requests,appium结合实现自动化测试

  2. 实现用例跳过skip和reruns失败用例重跑

  3. 可以结合allure-pytest插件生成allure报告

  4. 很方便和jenkins实现持续集成

  5. 有很多强大的插件:

    • pytest-html:生成html测试报告
    • pytest-xdist:多线程执行测试用例
    • pytest-ordering:改变测试用例的执行顺序
    • pytest-rerunfaiures:失败用例重跑
    • allure-pytest:生成allure报告

requirements.txt文档

1
2
3
4
5
6
7
pytest==7.2.0
pytest-html==4.1.1
pytest-xdist==3.5.0
pytest-ordering==0.6
pytest-rerunfailures==13.0
allure-pytest==2.13.2
pyyaml

pip install -r requirements.txt

pytest的最基本的测试用例规则

  1. 模块名必须以test_开头或者_test结尾
  2. 测试类必须以Test开头,并且不能带有init方法
  3. 测试用例必须以test_开头

命名规范:

  • 模块名:一般全小写,多个英文之间用_隔开
  • 类名:类名一般是首字母大写
  • 方法名:一般全小写,多个英文之间用_隔开

运行方式

主函数方式

1
2
3
4
import pytest

if __name__ == "__main__":
pytest.main()

常见参数:

  • -v:输出更加详细的信息,比如文件和用例的名称等
  • -s:输出调试信息,打印信息等
  • –reruns:失败重跑
  • -x:出现一个失败用例就停止测试
  • –maxfail=2:出现N个失败用例才终止测试
  • –html=report.html:生成html的测试报告
  • -n=2:N个线程执行
  • -k:运行测试用例名称中包含执行字符串的用例
    • pytest.main([‘-vs’,’-k’,’mengxun or william’])
    • pytest.main([‘-vs’,’-k’,’mengxun and william’])
指定模块运行
1
2
3
# 仅运行testcases文件夹下的test_api.py文件
if __name__ == "__main__":
pytest.main(['-vs','testcases/test_api.py'])
指定文件夹
1
2
3
# 运行testcases文件夹下的所有test文件
if __name__ == "__main__":
pytest.main(['-vs','testcases/'])
通过node id的方式运行测试用例
1
2
3
# 仅执行testcases文件夹下test_api.py文件中的Test_product_manage_1用例
if __name__ == "__main__":
pytest.main(['-vs', 'testcases/test_api.py::Test_product_manage_1'])

通过pytest.ini的配置文件运行

不管是命令行还是主函数都会读取这个配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[pytest]
#--html ./report/report.html,reruns 失败重跑次数
# addopts = -vs -n=2 -reruns 2 --html ./report/report.html
addopts = -vs --alluredir ./temp
# 指定 pytest 搜索测试文件的路径。pytest 将在 ./interfaceCase 目录下搜索可执行文件
testpaths =
./testYaml
; ./testApiCase
; ./interfaceCase
; ./testCase
# 指定 pytest 应该匹配的测试文件模式。pytest 将会运行所有以 test_ 为开头的 python 文件
# python_files = test_*.py
python_files = yaml_*.py
# 指定 pytest 应该匹配的测试类名模式。pytest 将会运行所有以 Test 开头的类
python_classes = Test
# 指定 pytest 应该匹配的测试函数模式。pytest 将会运行所有以 Test 开头的函数
python_functions = test
# 用于为测试用例添加标记。使用方法: @pytest.mark.smoke pytest -m "smoke [or usermanage]"
markers =
smoke : Smoke Testing Module
userManage : User Management Module
productManage : 商品管理

用例里面添加标记 @pytest.mark.smoke

1
2
3
4
5
6
7
class Test_login():

@pytest.mark.smoke
@pytest.mark.run(order=2)
def test_01(self,product_fixture):
time.sleep(0.5)
print("测试test_01")

执行的时候通过-m参数指定标记

addopts = ‐vs ‐m smoke

命令行方式

pytest

pytest -vs -n=2 ./testCase/test_login.py -m "smoke or userManage or productManage"

pytest -vs -n=2 ./testCase/test_login.py:test_01

pytest默认的执行测试用例顺序

pytest的默认执行顺序是从上到下的

改变默认用例的执行顺序,在用例上添加标记:@pytest.mark.run(order=1)

注意:有order装饰器的优先,相同的从上到下,然后再是没有装饰器的,负数不起作用。

跳过测试用例

  1. 无条件跳过

    1
    2
    3
    4
    @pytest.mark.skip(reason="无条件跳过")
    def test_02(self):
    time.sleep(0.5)
    print("测试test_02")
  2. 有条件跳过

    1
    2
    3
    4
    5
    6
    age = 19

    @pytest.mark.skipif(age >= 18, reason="有条件跳过 age >= 18")
    def test_04(self):
    time.sleep(0.5)
    print("测试test_04")

setup/teardown, setup_class/teardown_class

为什么需要这些功能?

比如:web自动化执行用例之前,请问需要打开浏览器吗?用例执行后需要关闭浏览器吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import pytest
import time

class Test_login():
age = 19

# 在所用的用例之前只执行一次, 比如:创建日志对象,创建数据库的连接,创建接口的请求对象
def setup_class(self):
print("\nsetup_class 在所有用例之前只执行一次")

# 在每一个用例之前都执行一次
def setup_method(self, method):
print("\n在执行测试用例之前执行的代码:" + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))

@pytest.mark.smoke
@pytest.mark.run(order=2)
def test_01(self):
time.sleep(3)
print("测试test_01")

@pytest.mark.userManage
@pytest.mark.run(order=1)
@pytest.mark.skip(reason="无条件跳过")
def test_02(self):
time.sleep(3)
print("测试test_02")

@pytest.mark.productManage
@pytest.mark.run(order=4)
def test_03(self):
time.sleep(3)
print("测试test_03")

@pytest.mark.skipif(age >= 18, reason="有条件跳过 age >= 18")
@pytest.mark.run(order=5)
def test_04(self):
time.sleep(3)
print("测试test_04")

@pytest.mark.run(order=3)
def test_05(self):
time.sleep(3)
print("测试test_05")

def teardown_class(self):
print("\nteardown_class在所有的用例之后,只执行一次")

def teardown_method(self, method):
print("\n在执行测试用例之后执行的代码:" + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))

注意:和Unittest不一样,全是小写

使用fixture装饰器来实现部分用例的前后置

可以指定部分用例执行setup或者teardown

@pytest.fixture(scope="", params="", autouse="", ids="", name="")

  • scope: 表示的是被@pytest.fixture标记的方法的作用域。function(默认), class, module, package/session

  • function: 在每个 def 前后执行一次。@pytest.fixture(scope="function")

  • class: 在每个 class 前后执行一次。@pytest.fixture(scope="class")

  • module: 在每个模块前后执行一次。@pytest.fixture(scope="module")

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @pytest.fixture(scope="function")
    def my_fixture():
    print("这是前置方法")
    yield
    print("这是后置方法")

    class TestInterface():

    def test_addition(self):
    assert add(3, 5) == 8
    assert add(-1, 1) == 0
    assert add(-1, -1) == -2

    def test_subtraction(self, my_fixture):
    assert subtract(5, 3) == 2
    assert subtract(-1, 1) == -2
    assert subtract(-1, -1) == 0
  • params: 参数化(支持:列表[],元组(),字典列表[{},{},{}],字典元组({},{},{}))

    request 和 request.param 是 pytest 的固定写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @pytest.fixture(scope="function", params=["蒙珣1", "蒙珣2", "蒙珣3"])
    def my_fixture(request):
    # return request.param
    print("前置")
    yield request.param # yield 后面返回可以接代码
    print("后置")

    class TestInterface():

    # 测试加法函数
    def test_addition(self):
    assert add(3, 5) == 8
    assert add(-1, 1) == 0
    assert add(-1, -1) == -2

    def test_subtraction(self, my_fixture):
    print("\n--------------------")
    print(str(my_fixture))
    print("--------------------\n")
    assert subtract(5, 3) == 2
    assert subtract(-1, 1) == -2
    assert subtract(-1, -1) == 0

    结果

    结果展示

  • autouse: 自动执行,默认False。搭配 scope 一起使用,可实现全部执行前后置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @pytest.fixture(scope="function", autouse=True)
    def my_fixture():
    print("这是前置方法")
    yield
    print("这是后置方法")

    class TestInterface():

    def test_addition(self):
    assert add(3, 5) == 8

    def test_subtraction(self):
    assert subtract(5, 3) == 2
  • ids: 当使用 params 参数化时,给每一个值设置一个变量名。意义不大。

    params 参数化那张图片的红框里面的 unicode 编码,我们可以使用 ids 来进行替换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @pytest.fixture(scope="function", 
    params=["蒙珣1", "蒙珣2", "蒙珣3"],
    ids=["mengxun1", "mengxun2", "mengxun3"])
    def my_fixture(request):
    # return request.param
    print("前置")
    yield request.param
    print("后置")

    class TestInterface():

    def test_addition(self):
    assert add(3, 5) == 8

    def test_subtraction(self, my_fixture):
    print("\n--------------------")
    print(str(my_fixture))
    print("--------------------\n")
    assert subtract(5, 3) == 2

    结果

    结果展示

  • name: 给被 @pytest.fixture 标记的方法取一个别名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @pytest.fixture(scope="function",
    params=["蒙珣1", "蒙珣2", "蒙珣3"],
    ids=["mengxun1", "mengxun2", "mengxun3"],
    name="abc")
    def my_fixture(request):
    # return request.param
    print("前置")
    yield request.param
    print("后置")

    class TestInterface():

    def test_addition(self):
    assert add(3, 5) == 8
    assert add(-1, 1) == 0
    assert add(-1, -1) == -2

    def test_subtraction(self, abc):
    print("\n--------------------")
    print(str(abc))
    print("--------------------\n")
    assert subtract(5, 3) == 2
    assert subtract(-1, 1) == -2
    assert subtract(-1, -1) == 0
    • 注意:fixture将方法起别名后,不能再引用原本方法名了

通过conftest.py和pytest.fixtrue()结合使用实现全局的前置应用

比如:项目的全局登录,模块的全局处理

  • conftest.py文件是单独存放的一个夹具配置文件,名称不能更改
  • 用处:可以在不同的py文件中使用同一个fixtrue函数
  • 原则上conftest.py需要和运行的用例放到统一层,并且不需要做import导入操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 目录结构
.
├── conftest.py # 作用于所有case文件夹下的全局文件
├── interfaceCase
│   ├── conftest.py # 作用于interfaceCase文件夹下的全局文件
│   ├── test_1.py
│   └── test_interface.py
├── main.py
├── pytest.ini
├── report
│   ├── assets
│   └── report.html
├── requirements.txt
└── testCase
├── __pycache__
├── conftest.py # 作用于testCase文件夹下的全局文件
└── test_login.py

下面是三个 conftest.py 文件,节省空间写到一段里了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 作用于interfaceCase文件夹下的全局文件
import pytest
@pytest.fixture(scope="function")
def user_fixture():
print("\n用户管理前置")
yield
print("\n用户管理后置")

# 作用于testCase文件夹下的全局文件
import pytest
@pytest.fixture(scope="function")
def product_fixture():
print("\n商品管理前置")
yield
print("\n商品管理前置")

# 作用于所有case文件夹下的全局文件
import pytest
@pytest.fixture(scope="function")
def all_fixture():
print("\n全局前置")
yield
print("\n全局后置")

如何引用 conftest.py 文件呢?

1
2
3
4
5
import time
class Test_1():
def test_01(self, all_fixture, user_fixture): # 引用全局fixture和user_fixtrue
time.sleep(0.5)
print("接口测试1")

结果

conftest.py

总结:

  • setup/teardown,setup_class/teardown_class 它作用于所有用例或者所有的类
  • @pytest.fixtrue() 它的作用既可以部分,也可以全部前后置
  • conftest.py 和 @pytest.fixture() 结合使用,作用于全局的前后置

断言

assert

1
2
3
4
5
6
class TestInterface():
# 测试加法函数
def test_addition(self, all_fixture, user_fixture):
assert add(3, 5) == 8
assert add(-1, 1) == 0
assert add(-1, -1) == -2

pytest结合allure-pytest插件生成allure测试报告

  • 比较丑的报告:pytest-html
  • 比较好看的报告:allure-pytest

配置allure环境

  1. 下载 allure ZIP包 https://github.com/allure-framework/allure2/releases
  2. pip install pytest-allure
  3. vim ~/.zshrc 并在最后一行添加环境变量 export PATH="/Users/william/DYJ/Tools/allure-2.7.0/bin:$PATH"
  4. 使环境变量生效 source ~/.zshrc
  5. 验证 allure --version

生成allure报告

  1. 修改pytest.ini文件中的addopts参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    [pytest]
    #--html ./report/report.html,reruns 失败重跑次数
    # addopts = -vs -n=2 -reruns 2 --html ./report/report.html
    # 生成json格式的allure报告
    addopts = -vs --alluredir ./temp
    # 指定 pytest 搜索测试文件的路径。pytest 将在 ./interfaceCase 目录下搜索可执行文件
    testpaths =
    ./interfaceCase
    ./testCase
    # 指定 pytest 应该匹配的测试文件模式。pytest 将会运行所有以 test_ 为开头的 python 文件
    python_files = test_*.py
    # 指定 pytest 应该匹配的测试类名模式。pytest 将会运行所有以 Test 开头的类
    python_classes = Test
    # 指定 pytest 应该匹配的测试函数模式。pytest 将会运行所有以 Test 开头的函数
    python_functions = test
    # 用于为测试用例添加标记。使用方法: @pytest.mark.smoke pytest -m "smoke [or usermanage]"
    markers =
    smoke : Smoke Testing Module
    userManage : User Management Module
    productManage : 商品管理
  2. 修改 mian.py 文件

    1
    2
    3
    4
    5
    6
    import os
    import pytest

    if __name__ == '__main__':
    pytest.main()
    os.system('allure generate ./temp -o ./report --clean')
    • allure generate 固定写法
    • ./temp: 临时的 json 格式报告的路径
    • -o: 输出 output
    • ./report: 生成的 allure 报告的路径
    • --clean: 清空./report 路径原来的报告
  3. 执行main.py文件 python3 main.py

@pytest.mark.parametrize数据驱动

@pytest.mark.parametrize(args_name, args_value)

  • args_name: 参数名

  • args_value: 参数值,有多少值就会执行多少次用例。(可以为:列表,元组,字典列表,字典元组)

  • 第一种方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import pytest

    class TestApi():
    @pytest.mark.parametrize("args", ["mengxun1", "mengxun2", "mengxun3"])
    def test_api_01(self, args):
    print(args)

    if __name__ == "__main__":
    pytest.main()
  • 第二种方式:跟unittest的ddt里面的@unpack解包一样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import pytest

    class TestApi():
    @pytest.mark.parametrize("name, age", [["mengxun1", "25"], ["mengxun2", "26"]])
    def test_api_02(self, name, age):
    print(name, age)

    if __name__ == "__main__":
    pytest.main()

结果

parametrize

YAML语法

yaml文件的基础语法在这篇文章YAML语法中有写,这里就不在赘述了

如何使用yaml文件呢?

  1. 安装pyyaml,pip install pyyaml

  2. 编写yaml文件

    1
    2
    3
    mengxun:
    - name: 小苍鹰
    - age: 25
  3. 调用yaml文件,并打印

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import yaml

    class YamlUntil():
    def __init__(self, yaml_file):
    self.yaml_file = yaml_file

    # 读取yaml文件
    def read_yaml(self):
    with open(self.yaml_file, 'r', encoding='utf-8') as f:
    data = yaml.load(f, Loader=yaml.FullLoader)
    print(data)

    if __name__ == '__main__':
    YamlUntil('yaml_until.yaml').read_yaml()
  4. 运行yaml_until.py文件

  5. 结果

yaml接口自动化练习

  1. 编写yaml文件,包含用例名称、请求头,请求体

    一条搜索新闻,一条搜索B站

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    # test_api.yaml
    -
    name: bing搜索keywords
    request:
    url: https://cn.bing.com/search
    method: get
    headers:
    Content-Type: application/json
    params:
    go: 搜索
    q: 今日新闻
    # validate:
    # - eq: {returnValue: 200}

    -
    name: bing搜索keywords
    request:
    url: https://cn.bing.com/search
    method: get
    headers:
    Content-Type: application/json
    params:
    go: 搜索
    q: B站
    # validate:
    # - eq: {returnValue: 200}
  2. 编写类方法,读取yaml文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import yaml

    class YamlUntil():
    def __init__(self, yaml_file):
    self.yaml_file = yaml_file

    # 读取yaml文件
    def read_yaml(self):
    with open(self.yaml_file, 'r', encoding='utf-8') as f:
    data = yaml.load(f, Loader=yaml.FullLoader) # 加载yaml
    return data
  3. 使用装饰器parametrize

    test_api.yaml 文件的返回格式是:{'name': 'bing搜索keywords', 'request': {'url': 'https://cn.bing.com/search', 'method': 'get', 'headers': {'Content-Type': 'application/json'}, 'params': {'go': '搜索', 'q': '今日新闻'}}}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import os
    import pytest
    import requests
    import yaml

    class YamlUntil():
    def __init__(self, yaml_file):
    ...
    return data

    class TestYamlApi():
    # os.getcwd() 获取当前文件路径,执行的时候,一直说找不到yaml文件
    @pytest.mark.parametrize("args", YamlUntil(os.getcwd() + '/testYaml/test_api.yaml').read_yaml())
    def test_yaml_01(self):
    # print(args) # 打印出yaml返回格式
    name = args['name']
    url = args['request']['url']
    params = args['request']['parmas']
    res = requests.get(url, params=params)
    assert res.status_code == 200
    print('\nname: ' + name)
    print(res.text[:100])

    if __name__ == '__main__':
    # YamlUntil('yaml_until.yaml').read_yaml()
    TestYamlApi().test_yaml_01()
  4. 结果

    yaml接口测试练习.jpg