Pytest框架

一、pytest入门

1.简介
  • pytest是Python的一个单元测试框架,与python自带的unittest测试框架类似;
  • pytest比unittest框架使用起来更简洁,效率更高,而且特性比较多,也就非常的灵活;

pytest比unittest框架区别:

  • 语法和风格:
    • pytest 的语法更简洁和灵活,测试函数不需要继承特定的类,只需要以 test_ 开头即可。
    • unittest 要求测试用例继承自 unittest.TestCase 类,测试方法必须以 test 开头。
  • 断言方式:
    • pytest 可以使用标准的 Python 断言语句,更直观和简洁。
    • unittest 则需要使用特定的断言方法,如 assertEqualassertTrue 等。
  • fixtures(测试夹具):
    • pytest 中的 fixtures 功能更强大和灵活,可以通过参数自动注入,并且可以有更复杂的范围控制(如 functionclassmodulesession )。
    • unittest 也有类似的 setUp 和 tearDown 方法,但功能相对较简单。
  • 参数化:
    • pytest 的参数化更简单直观,使用 @pytest.mark.parametrize 装饰器。
    • unittest 实现参数化相对复杂。
  • 插件生态:
    • pytest 拥有丰富的插件生态系统,可以方便地扩展其功能。
    • unittest 的插件相对较少。
  • 执行方式:
    • pytest 可以自动发现测试用例,无需显式指定测试模块。
    • unittest 通常需要在测试脚本中明确指定要运行的测试用例。
  • 错误报告:
    • pytest 的错误报告通常更详细和易于理解。
  • 比如pytest常用的特性有:

① 对case可以进行设置跳过,也可以进行标记(比如失败等);

② 可以重复执行失败的case;

③ 可以兼容执行unittest编写的case;

④ 有很多第三方的插件,比如报告allure等;

⑤ 支持持续集成;

⑥ 和unittest一样支持参数化,但Pytest方法更多,更灵活;

2.官方文档

Full pytest documentation - pytest documentation

3.pytest框架约束

  • 模块名(py文件)必须是以test_开头或者_test结尾
  • 测试类(class)必须以Test开头,并且不能带init方法,类里的方法必须以test_开头
  • 测试用例(函数)必须以test_开头

4.pytest运行方式
4.1 主函数运行
import pytest


def test_01():
    print('这个是测试用例一')
    assert 1 == 1


if __name__ == '__main__':
    pytest.main([])

4.2 命令行运行

4.3 main中可用参数
-v 输出信息
  • 如:文件名/用例名信息
if __name__ == '__main__':
    pytest.main(["-v"])

-s 输出调试信息
  • 如:打印信息
if __name__ == '__main__':
    pytest.main(["-s"])

-x 用例失败停止执行
  • 只要有一个用例执行失败,就停止执行测试
import pytest


def test_01():
    assert 1 == 1
    print('这是成功用例')


def test_02():
    assert 2 == 1


def test_03():
    assert 2 == 3


def test_04():
    assert 4 == 4


if __name__ == '__main__':
    pytest.main(["-vs", "-x"])

  • --maxfail=N 出现N个测试用例失败,就停止测试
import pytest


def test_01():
    assert 1 == 1
    print('这是成功用例')


def test_02():
    assert 2 == 1


def test_03():
    assert 2 == 3


def test_04():
    assert 4 == 4


if __name__ == '__main__':
    pytest.main(["-vs", "--maxfail=2"])

-m 通过标记表达式执行
import pytest


@pytest.mark.wjh
def test_01():
    assert 1 == 1
    print('这是成功用例')

def test_02():
    assert 2 == 1


def test_03():
    assert 2 == 3


def test_04():
    assert 4 == 4


if __name__ == '__main__':
    pytest.main(["-vs", "-m wjh"])

-k 指定用例执行
import pytest


def test_01():
    assert 1 == 1
print('这是成功用例')


def test_02():
    assert 2 == 1


class TestCls:

    def test1_02(self):
        assert 2 == 3

def test_14(self):
    assert 4 == 4

  • -k
if __name__ == '__main__':
    pytest.main(["-vs", "-k _02"])

  • -k or
if __name__ == '__main__':
    pytest.main(["-vs", "-k test_01 or test_02"])

  • -k and
if __name__ == '__main__':
    pytest.main(["-vs", "-k test_ and 02"])

  • -k and not
if __name__ == '__main__':
    pytest.main(["-vs", "-k test_ and not 02"])

4.4 ini配置文件运行

参数

作用

[pytest]

用于标志这个文件是pytest的配置文件

addopts

命令行参数,多个参数之间用空格分隔

testpaths

配置搜索参数用例的范围

python_files

改变默认的文件搜索规则

python_classes

改变默认的类搜索规则

python_functions

改变默认的测试用例的搜索规则

markers

用例标记,自定义mark,需要先注册标记,运行时才不会出现warnings

filterwarnings

配置运行用例时忽略的警告信息

[pytest]

addopts = -vs -m

testpaths =
test_scenario

python_files = test*.py

;配置测试搜索的测试类名
python_classes = Test*

;配置测试搜索的测试函数名
python_functions = test

markers =
wjh: 'wjh标签'


filterwarnings =
ignore::pytest.PytestAssertRewriteWarning
ignore::UserWarning
ignore::RuntimeWarning
ignore::DeprecationWarning
5.插件
控制用例执行顺序
  • 需下载pytest-ordering插件,数字越小越靠前执行
import pytest


@pytest.mark.run(order=2)
def test_01():
    print('这个是测试用例一')
    assert 1 == 1


@pytest.mark.run(order=1)
def test_02():
    print('这个是测试用例二')
    assert 2 == 2


if __name__ == '__main__':
    pytest.main(['-vs'])

控制失败用例重跑
  • 需下载pytest-rerunfailures插件。使用 --reruns nums(nums代表重跑次数)
import pytest


def test_01():
    print('这个是测试用例一')
    assert 1 == 1


def test_02():
    print('这个是测试用例二')
    assert 1 == 2


if __name__ == '__main__':
    pytest.main(['-vs'])

控制用例多线程执行
  • -n 分布式运行测试用例-需下载xdist插件
import pytest


def test_01():
    assert 1 == 1
    print('这是成功用例')


def test_02():
    assert 2 == 2


def test_03():
    assert 3 == 3


def test_04():
    assert 4 == 4


if __name__ == '__main__':
    pytest.main(['-n=2'])

6.断言:assert
import pytest


def test_01():
    print('这个是测试用例一')
    assert 1 == 1


def test_02():
    print('这个是测试用例二')
    assert 1 == 2


if __name__ == '__main__':
    pytest.main(['-vs'])

  • 断言分为接口断言和业务断言两种
import pytest
import requests


def test_01():
    res = requests.post(url='https://test-zjyth.youmatech.com/n0/api/common-login/auth/public_key')
    assert res.status_code == 200  # 接口断言
    response = res.json()
    assert response.get('code') == 3001  # 业务断言
    assert response.get('msg') is None  # 业务断言


if __name__ == '__main__':
    pytest.main([''])

断言

说明

assert a==b

判断a等于b

assert a!=b

判断a不等于b

assert a>b

判断a大于b

assert a<b

判断a小于b

assert a in b

判断b包含a

assert a not in b

判断b不包含a

assert a is true

判断a为真

assert a is false

判断a为假

assert a is None

判断a为空

assert a is not None

判断a不为空

7.基于pytest框架自身的前置后置
  • 一个完整的单元测试用例并不是仅仅有测试执行就可以的,还需要执行测试之前的前置工作以及执行测试之后的后置工作
在类以外使用的前置与后置

setup_module()/teardown_module()

setup_function()/teardown_function()

import pytest


def setup_module():
    print("在整个模块前运行一次")


def teardown_module():
    print("在整个模块后运行一次")


def setup_function():
    print("在每个测试函数前运行一次")


def teardown_function():
    print("在整个测试函数后运行一次")


def test_01():
    print('这个是测试用例一')


def test_02():
    print('这个是测试用例一')


if __name__ == '__main__':
    pytest.main(['-vs'])

在类以内使用的前置与后置

setup_class()/teardown_class()

setup_method()/teardown_method()

import pytest


def setup_module():
    print("在整个模块前运行一次")


def teardown_module():
    print("在整个模块后运行一次")


def setup_function():
    print("在每个测试函数前运行一次")


def teardown_function():
    print("在整个测试函数后运行一次")


def test_01():
    print('这个是测试用例一')


def test_02():
    print('这个是测试用例二')


class TestCase:
    @staticmethod
    def setup_class():
        print('在整个测试类前执行一次')

    @staticmethod
    def teardown_class():
        print('在整个测试类后执行一次')

    @staticmethod
    def setup_method():
        print('类里面每个测试方法前执行一次')

    @staticmethod
    def teardown_method():
        print('类里面每个测试方法后执行一次')

    def test_03(self):
        print('这个是测试用例三')

    def test_04(self):
        print('这个是测试用例四')


if __name__ == '__main__':
    pytest.main(['-vs'])

二、pytest中fixture装饰器

1.什么是fixture

Fixture 可以被看作是测试用例的一种辅助工具,它可以提供测试所需的各种资源,例如数据库连接、文件内容、模拟对象等。通过使用 Fixture,可以将测试用例与这些复杂的准备和清理工作分离,使测试代码更加简洁、可维护和可重复使用。

2.Fixture的优势
  • 命名方式灵活,不限于setup和teardown两种命名
  • conftest.py可以实现数据共享,不需要执行import 就能自动找到fixture
  • scope=module,可以实现多个.py文件共享前置
  • scope=“session” 以实现多个.py 跨文件使用一个 session 来完成多个用例
3.Fixture调用方式
@pytest.fixture(scope='function', params=None, autouse=False, ids=None, name=None)
def test_02():
    print('这个是测试用例二')
    assert 2 == 2
4.作用范围

scope取值

范围说明

function

一个函数调用一次

class

一个类调用一次

module

整个.py文件调用一次

package

整个文件夹调用一次

session

全局调用一次

5.Fixture参数说明
5.1 scope:作用域
  • scope='function' 方法级,每个方法执行一次

import pytest


@pytest.fixture()
def setup_01():
    print('这个是测试前置1')


class TestCase01:

    def test_01(self, setup_01):
        print('这个是测试用例1')

    def test_02(self, setup_01):
        print('这个是测试用例2')

  • scope='class' 类级,每个类执行一次
import pytest


class TestCase01:

    def test_01(self, setup):
        print('这个是测试用例1')

    def test_02(self, setup):
        print('这个是测试用例2')


class TestCase02:

    def test_01(self, setup):
        print('这个是测试用例3')

    def test_02(self, setup):
        print('这个是测试用例4')


if __name__ == '__main__':
    pytest.main(['-vs'])

  • scope='module' 模块级,每个模块执行一次
  • scope='package' 包级,每个包执行一次
  • scope='session' 会话级,每此会话执行一次
  • 不写默认 'function'
5.2.autouse

自动运行,默认是False

5.3.params和ids

params和自定义参数ids在参数化中介绍使用

5.4.name

给fixture起别名。

6.Fixture手动调用
  • 方法一:通过传参实现

讲fixture传入方法中,在运行该方法用例之前,会先自动运行该fixture

import pytest


@pytest.fixture()
def setup_01():
    print('这个是测试前置1')


class TestCase01:

    def test_01(self, setup_01):
        print('这个是测试用例1')

    def test_02(self, setup_01):
        print('这个是测试用例2')

  • 方法二:通过@pytest.mark.usefixtures装饰器实现
# @Time : 2024/7/22 20:09  
# @Author : Jacky Wang
# @File : test_04.py
import pytest


@pytest.fixture()
def setup_01():
    print('这个是测试前置1')


class TestCase01:
    @pytest.mark.usefixtures('setup_01')
    def test_01(self):
        print('这个是测试用例1')

    @pytest.mark.usefixtures('setup_01')
    def test_02(self):
        print('这个是测试用例2')


if __name__ == '__main__':
    pytest.main(['-vs'])

7.基于装饰器的一种特殊的前置后置
import pytest


@pytest.fixture()
def setup_and_teardown():
    print("这个是测试前置")
    yield
    print("这个是测试后置")


def test_01(setup_and_teardown):
    print('这个是测试用例一')


def test_02(setup_and_teardown):
    print('这个是测试用例二')


if __name__ == '__main__':
    pytest.main(['-vs'])

8.@pytest.mark.skip/@pytest.mark.skipif跳过用例
  • @pytest.mark.skip
import pytest


def test_01():
    print('这个是测试用例一')
    assert 1 == 1


@pytest.mark.skip(reason='失败用例不执行')
def test_02():
    print('这个是测试用例二')
    assert 1 == 2


if __name__ == '__main__':
    pytest.main(['-vs'])

  • @pytest.mark.skipif
import pytest


def test_01():
    print('这个是测试用例一')
    assert 1 == 1


@pytest.mark.skipif(condition=True, reason='条件为真则不执行')
def test_02():
    print('这个是测试用例二')
    assert 1 == 2


if __name__ == '__main__':
    pytest.main(['-vs'])

三、conftest.py

1.conftest
1.1 什么是conftest.py

可以理解成一个专门存在fixture的配置文件

1.2 实际开发场景

例如:多个测试用例文件(test_*.py)的所有用例都需要用登录功能作为前置操作,因此就不能把登录功能写到某个用例文件中

1.3 conftest.py的特点
  • pytest会默认读取conftest.py里面的所有fixture
  • conftest.py文件名称是固定的,不能改动
  • conftest.py只对同一个package下的所有测试用例生效
  • 不同目录可以有自己的conftest.py,一个项目中可以有多个conftest.py
  • 测试用例文件中不需要手动import conftest.py,pytest会自动查找
1.4 conftest作用域
  • session作用域级别conftest
  • package作用域级别conftest
  • module作用域级别conftest
  • class作用域级别conftest
  • function作用域

四、参数化

1.什么是参数化

在测试框架(如 pytest)中,可以使用参数化来运行同一个测试函数多次,每次使用不同的输入参数,从而覆盖更多的测试场景

2.参数化的实战使用
  • @pytest.mark.parametrize() 装饰器接收两个参数
  • 第一个参数是以字符串的形式标识用例函数的参数
  • 第二个参数是以可迭代对象的形式传递测试数据,一般使用list或tuple
import requests
import pytest


def base_data() -> list:
    return [
        (
            "test",
            "wangjunhong",
            "123456",
            3001
        ),
        (
            "test",
            "wangjunhong",
            "1234567",
            3001
        ),
        (
            "test",
            "wangjunhong1",
            "123456",
            3001
        )
    ]


class TestLogin:

    @pytest.mark.parametrize("businessCode, loginName, password, expect", base_data())
    def test_pc_login(self, businessCode, loginName, password, expect):
        url = 'https://test.i.youmatech.com/api/login/p/submit'
        data = {
            "businessCode": businessCode,
            "loginName": loginName,
            "password": password,

        }
        headers = {"Content-Type": "application/json"}
        res = (requests.post(url=url, headers=headers, json=data)).json()
        assert res.get('code') == expect

  • @pytest.mark.parametrize:ids

  • 通过上面的执行结果我们不难看出,用例是通过将参数进行拼接的形式来展示的,如果参数不多时,我们还比较容易看出;如果参数过多时,你还能很明显的看出此条用例期望的执行结果吗?
  • 此时我们将会使用另一个参数ids来自定义显示结果:ids必须是与数据数量相同的名称(必须是字符串)列表
# @Time : 2024/7/22 20:09  
# @Author : Jacky Wang
# @File : test_04.py
# encoding='utf-8'
import requests
import pytest


def base_data() -> tuple:
    return (
        (
            "test",
            "wangjunhong",
            "123456",
            3001
        ),
        (
            "test",
            "wangjunhong",
            "1234567",
            3001
        ),
        (
            "test",
            "wangjunhong1",
            "123456",
            3001
        )
    )


class TestLogin:

    @pytest.mark.parametrize("businessCode, loginName, password, expect", base_data(),
                             ids=["login_success", 'login_fail', 'login_fail'])
    def test_pc_login(self, businessCode, loginName, password, expect):
        url = 'https://test.i.youmatech.com/api/login/p/submit'
        data = {
            "businessCode": businessCode,
            "loginName": loginName,
            "password": password,

        }
        headers = {"Content-Type": "application/json"}
        res = (requests.post(url=url, headers=headers, json=data)).json()
        assert res.get('code') == expect


# class TestGetName:
#
#     @pytest.mark.parametrize("name", ['张三', '李四', '王五'])
#     def test_get_name(self, name):
#         print(name)
#
#
# class TestGetNameAndAge:
#
#     @pytest.mark.parametrize("name, age", [('张三', 20), ('李四', 30), ('王五', 40)])
#     def test_get_name(self, name, age):
#         print(name, age)


if __name__ == '__main__':
    pytest.main(['-vs'])

五、持续集成

1.pytest+jenkins+allure

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值