测试笔记:学习Pytest框架

pip install pytest -i https://mirrors.aliyun.com/pypi/simple/

2.1 检索规则

  • 运行 test_ 或 * _test.py* 形式的所有目录、文件或函数。
  • 运行 Test*开头的类。

2.2 运行方式

python -m pytest [参数] [路径]
pytest [参数] [路径]
import pytest

if __name__ == "__main__":
    pytest.main(["", "执行路径"])

夹具,用来 构造测试结构,例如”前置 – 测试 – 后置”的结构,可以用在登录、数据库连接、清理数据等场景。

3.1 作用域

相同测试下仅创建一次fixture,测试执行完后,fixture销毁。

目录:
WorkingDirection/
└────test_fixture/
│    ├────__init__.py
│    └────test_fixture_scope.py

代码:

import pytest

@pytest.fixture(scope="function")
def get_person():
    return Person()

def test_1(get_person):
    print(id(get_person))

def test_2(get_person):
    print(id(get_person))

运行结果:
============================= test session starts =============================
collecting ... collected 2 items

test_fixture_scope.py::test_1 PASSED                                     [ 50%]2713523477768

test_fixture_scope.py::test_2 PASSED                                     [100%]2713523676808

============================== 2 passed in 0.01s ==============================

相同类下仅创建一次fixture,类下最后一条测试执行完后,fixture销毁。

目录:
WorkingDirection/
└────test_fixture/
│    ├────__init__.py
│    └────test_fixture_scope2.py

代码:

import pytest

class Person:
    pass

@pytest.fixture(scope="class")
def get_person():
    return Person()

class Test_1:

    def test_1(self, get_person):
        print(id(get_person))

    def test_2(self, get_person):
        print(id(get_person))

class Test_2:

    def test_3(self, get_person):
        print(id(get_person))

    def test_4(self, get_person):
        print(id(get_person))

运行结果:
============================= test session starts =============================
collecting ... collected 4 items

test_fixture_scope2.py::Test_1::test_1 PASSED                            [ 25%]2139773693128

test_fixture_scope2.py::Test_1::test_2 PASSED                            [ 50%]2139773693128

test_fixture_scope2.py::Test_2::test_3 PASSED                            [ 75%]2139773421640

test_fixture_scope2.py::Test_2::test_4 PASSED                            [100%]2139773421640

============================== 4 passed in 0.01s ==============================

相同模块下仅创建一次fixture,模块下最后一条测试执行完后,fixture销毁。

目录:
WorkingDirection/
└────test_fixture/
│    ├────__init__.py
│    ├────conftest.py
│    ├────test_fixture_scope3.py
│    └────test_fixture_scope4.py

代码:

import pytest

class User:
    pass

@pytest.fixture(scope="module")
def get_user():
    return User()

def test_1(get_user):
    print(id(get_user))

def test_2(get_user):
    print(id(get_user))

def test_3(get_user):
    print(id(get_user))

def test_4(get_user):
    print(id(get_user))

运行结果(test_fixture_scope3.py):
============================= test session starts =============================
collecting ... collected 2 items

test_fixture_scope3.py::test_1 PASSED                                    [ 50%]2713270226440

test_fixture_scope3.py::test_2 PASSED                                    [100%]2713270226440

============================== 2 passed in 0.01s ==============================

运行结果(test_fixture_scope4.py):
============================= test session starts =============================
collecting ... collected 2 items

test_fixture_scope4.py::test_3 PASSED                                    [ 50%]2730364832968

test_fixture_scope4.py::test_4 PASSED                                    [100%]2730364832968

============================== 2 passed in 0.01s ==============================

整个项目下仅创建一次fixture,测试执行完后,fixture销毁。

目录:
WorkingDirection/
└────test_fixture/
│    ├────__init__.py
│    ├────conftest.py
│    └────test_scope_session/
│    │    ├────__init__.py
│    │    ├────test_fixture_scope5.py
│    │    └────test_fixture_scope6.py

代码:

import pytest

class Fruit:
    pass

@pytest.fixture(scope="session")
def get_fruit():
    return Fruit()

def test_1(get_fruit):
    print(id(get_fruit))

def test_2(get_fruit):
    print(id(get_fruit))

def test_3(get_fruit):
    print(id(get_fruit))

def test_4(get_fruit):
    print(id(get_fruit))

运行结果:
============================= test session starts =============================
collecting ... collected 4 items

test_fixture_scope5.py::test_1 PASSED                                    [ 25%]3068522292168

test_fixture_scope5.py::test_2 PASSED                                    [ 50%]3068522292168

test_fixture_scope6.py::test_3 PASSED                                    [ 75%]3068522292168

test_fixture_scope6.py::test_4 PASSED                                    [100%]3068522292168

============================== 4 passed in 0.02s ==============================

Process finished with exit code 0

3.2 可缓存

对于当此运行的测试而言,fixture仅会在作用域内创建一次,重复调用会使用缓存值,而不再执行。

import pytest

num = []

@pytest.fixture
def f1():
    print("\n执行了f1...")
    num.append("a")
    return num

@pytest.fixture
def f2(f1):
    print("执行了f2...")
    num.append("b")
    return num

def test_1(f2, f1):
    print(num)

运行结果:
============================= test session starts =============================
collecting ... collected 1 item

test_fixture_cache.py::test_1
执行了f1...

执行了f2...

PASSED                                     [100%]['a', 'b']

============================== 1 passed in 0.02s ==============================

3.3 可参数化

可使用 request来接收fixture的返回值, request.param来接收fixture的参数内容。

import pytest

class User:

    def __init__(self, name, age):
        self.name = name
        self.age = age

data = [
    {"name": "zhangsan", "age": 18},
    {"name": "lisi", "age": 88}
]

@pytest.fixture(params=data, ids=["用户a", "用户b"])
def get_user(request):
    return User(request.param["name"], request.param["age"])

def test_1(get_user):
    print(get_user)

运行结果:
============================= test session starts =============================
collecting ... collected 2 items

test_fixture_param.py::test_1[\u7528\u6237a] PASSED                      [ 50%]<test_fixture.test_fixture_param.User object at 0x0000024104C24508>

test_fixture_param.py::test_1[\u7528\u6237b] PASSED                      [100%]<test_fixture.test_fixture_param.User object at 0x0000024104CE0188>

============================== 2 passed in 0.01s ==============================

3.4 可嵌套

注意:必须用小作用域的fixture去嵌套大作用域的fixture,否则会报错,如下:

import pytest

num = []

@pytest.fixture
def f1():
    print("\n执行了f1...")
    num.append("a")
    return num

@pytest.fixture(scope="class")
def f2(f1):
    print("执行了f2...")
    num.append("b")
    return num

def test_1(f2, f1):
    print(num)

运行结果:
============================= test session starts =============================
collecting ... collected 1 item

test_fixture_cache.py::test_1 ERROR                                      [100%]
test setup failed
ScopeMismatch: You tried to access the 'function' scoped fixture 'f1' with a 'class' scoped request object, involved factories
test_fixture_cache.py:13:  def f2(f1)
test_fixture_cache.py:6:  def f1()

============================== 1 error in 0.02s ===============================

3.5 yield返回

import pytest

class Test_basket:

    basket = None

    @pytest.fixture(scope="class")
    def create_basket(self):
        Test_basket.basket = []
        print(f"\n拿来一个篮子: {Test_basket.basket}")
        yield Test_basket.basket
        Test_basket.basket = None
        print(f"\n归还一个篮子: {Test_basket.basket}")

    @pytest.fixture
    def add_apple(self):
        Test_basket.basket.append("苹果")
        print("\n添加了苹果")
        yield
        Test_basket.basket.remove("苹果")
        print("拿走了苹果")

    @pytest.fixture
    def add_banana(self):
        Test_basket.basket.append("香蕉")
        print("添加了香蕉")
        yield
        Test_basket.basket.remove("香蕉")
        print("拿走了香蕉")

"""
        create_basket是类级作用域,add_apple、add_banana是函数级作用域,作用域大的优先执行。
"""
    def test_1(self, add_apple, add_banana, create_basket):
        print(f"\n检查篮子中的水果: {Test_basket.basket}")

运行结果:
============================= test session starts =============================
collecting ... collected 1 item

test_fixture_yield.py::Test_basket::test_1
拿来一个篮子: []

添加了苹果
添加了香蕉
PASSED                        [100%]
检查篮子中的水果: ['苹果', '香蕉']
拿走了香蕉
拿走了苹果

归还一个篮子: None

============================== 1 passed in 0.01s ==============================

3.5 可自适应

不用在测试参数中传入fixture名称,fixture会自动调用。

import pytest

class Product:
    pass

@pytest.fixture(autouse=True)
def get_product():
    print("生产了一个零件")
    return Product()

def test_1():
    print("执行test_1...")

def test_2():
    print("执行test_2...")

运行结果:
============================= test session starts =============================
collecting ... collected 2 items

test_fixture_auto.py::test_1 生产了一个零件
PASSED                                      [ 50%]执行test_1...

test_fixture_auto.py::test_2 生产了一个零件
PASSED                                      [100%]执行test_2...

============================== 2 passed in 0.01s ==============================
  • monkeypatch是pytest内置的fixture,可以用来mock数据。
  • monkeypatch仅会在代码运行时(内存中)起作用,不会修改源码,因此只对当前的测试生效。
import pytest

class A:
    info = {
        "name": "小A",
        "age": 18
    }

    @classmethod
    def open_website(cls, url):
        print(f"\n{cls.info['name']}打开了网站{url}")

class B:

    @classmethod
    def open_website(cls, url):
        print(f"\nB 打开了网站{url}")

@pytest.fixture
def mock_url(monkeypatch):
    monkeypatch.setattr(A, "open_website", B.open_website)
    monkeypatch.setitem(A.info, "age", 80)

a = A()

def test_1(mock_url):
    a.open_website("https://www.baidu.com")
    print(a.info)

"""
    test_2运行结果说明猴子补丁不会修改源码。
"""

def test_2():
    a.open_website("https://www.baidu.com")
    print(a.info)

运行结果:
============================= test session starts =============================
collecting ... collected 2 items

test_fixture_mock.py::test_1 PASSED                                      [ 50%]
B 打开了网站https://www.baidu.com
{'name': '小A', 'age': 80}

test_fixture_mock.py::test_2 PASSED                                      [100%]
小A打开了网站https://www.baidu.com
{'name': '小A', 'age': 18}

============================== 2 passed in 0.01s ==============================

5.1 给测试打上标签

通过打标签,可以选择性得运行测试,需要注意,标记需要先注册!

目录:
WorkingDirection/
├────__init__.py
├────main.py
├────pytest.ini
└────test_mark.py

[pytest]
markers=
    smoke : "run smoke test"

import pytest

@pytest.mark.smoke
def test_1():
    print("运行了test_1...")

@pytest.mark.smoke
def test_2():
    print("运行了test_2...")

def test_3():
    print("运行了test_3...")

import pytest

if __name__ == "__main__":
    pytest.main(["-m smoke", "test_mark.py"])

运行结果(main.py):
============================= test session starts =============================
platform win32 -- Python 3.7.8, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: S:\study\studyPytest\test_mark, configfile: pytest.ini
plugins: html-2.1.1, metadata-1.11.0
collected 3 items / 1 deselected / 2 selected

test_mark.py ..                                                          [100%]

======================= 2 passed, 1 deselected in 0.02s =======================

5.2 测试参数化

import pytest

@pytest.mark.parametrize("user,password",
                         [("admin", "132456"), ("wangwu", "456789")],
                         ids=["管理员登录", "用户登录"])
def test_1(user, password):
    print(f"登录者:{user}-{password}")

"""
    可以做正交
"""

@pytest.mark.parametrize("sex", ["男", "女"])
@pytest.mark.parametrize("name", ["张三", "李四", "王五"])
def test_2(name, sex):
    print(f"查询条件:{name} + {sex}")

运行结果:
============================= test session starts =============================
collecting ... collected 8 items

test_mark2.py::test_1[\u7ba1\u7406\u5458\u767b\u5f55] PASSED             [ 12%]登录者:admin-132456

test_mark2.py::test_1[\u7528\u6237\u767b\u5f55] PASSED                   [ 25%]登录者:wangwu-456789

test_mark2.py::test_2[\u5f20\u4e09-\u7537] PASSED                        [ 37%]查询条件:张三 + 男

test_mark2.py::test_2[\u5f20\u4e09-\u5973] PASSED                        [ 50%]查询条件:张三 + 女

test_mark2.py::test_2[\u674e\u56db-\u7537] PASSED                        [ 62%]查询条件:李四 + 男

test_mark2.py::test_2[\u674e\u56db-\u5973] PASSED                        [ 75%]查询条件:李四 + 女

test_mark2.py::test_2[\u738b\u4e94-\u7537] PASSED                        [ 87%]查询条件:王五 + 男

test_mark2.py::test_2[\u738b\u4e94-\u5973] PASSED                        [100%]查询条件:王五 + 女

============================== 8 passed in 0.05s ==============================

5.3 跳过测试

可以直接跳过测试,或者满足某种条件时,跳过测试(测试不会被执行)。

from random import random

import pytest

i = int(random() * 10)

@pytest.mark.skip("测试作废")
def test_1():
    print("test_1 执行了...")

@pytest.mark.skipif(f"{i} > 5")
def test_2():
    print("test_1 执行了...")

运行结果:
============================= test session starts =============================
collecting ... collected 2 items

test_mark_skip.py::test_1 SKIPPED (测试作废)                             [ 50%]
Skipped: 测试作废

test_mark_skip.py::test_2 SKIPPED (condition: 8 > 5)                     [100%]
Skipped: condition: 8 > 5

============================= 2 skipped in 0.01s ==============================

作用是 期望测试用例是失败的(比如已知前置条件会出问题,设计该测试时可以让这个测试预期失败),若测试执行失败,则结果为xfailed,若测试执行成功,则结果为xpassed,除此之外,xfail与skip不同的是,xfail的测试是会被执行的,而skip是直接跳过测试。

from random import random

import pytest

@pytest.mark.xfail
def test_1():
    print("执行了test_1...")
    assert 1

@pytest.mark.xfail
def test_2():
    print("执行了test_2...")
    assert 0

"""
    test_3中,pytest.xfail 后面的代码不会被执行。
"""

def test_3():
    i = random()*10
    print("开始执行test_3...")
    if i > 5:
        pytest.xfail("功能未完成,停止执行")
    print("执行了test_3...")

运行结果:
============================= test session starts =============================
collecting ... collected 3 items

test_mark_xfail.py::test_1 XPASS                                         [ 33%]执行了test_1...

test_mark_xfail.py::test_2 XFAIL                                         [ 66%]执行了test_2...

@pytest.mark.xfail
    def test_2():
        print("执行了test_2...")
>       assert 0
E       assert 0

test_mark_xfail.py:15: AssertionError

test_mark_xfail.py::test_3 XFAIL (功能未完成,停止执行)                  [100%]开始执行test_3...

def test_3():
        i = random()*10
        print("开始执行test_3...")
        if i > 5:
>           pytest.xfail("功能未完成,停止执行")
E           _pytest.outcomes.XFailed: 功能未完成,停止执行

test_mark_xfail.py:27: XFailed

======================== 2 xfailed, 1 xpassed in 0.02s ========================

钩子函数,即 Hook函数,本质上是 回调函数(即函数当作参数传入,例如,函数A的参数接收函数B,那么函数B在函数A内部被调用了,则函数B就是回调函数,好处是,实现了函数之间的解耦,函数A不需要关注函数B是如何实现的),Pytest提供了许多Hook函数,在测试生命周期(初始化 -> 测试收集 -> 测试执行 -> 输出报告)的特定阶段会 自动调用,在 conftest.py文件中 实现Hook函数,从而对测试阶段进行干预。

6.1 解决ids控制台显示乱码

目录:
WorkingDirection/
├────__init__.py
├────conftest.py
└────test_hook.py

def pytest_collection_modifyitems(items):
    for item in items:

        item.name = item.name.encode("utf-8").decode("unicode_escape")
        item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape")

import pytest

@pytest.mark.parametrize("data", ["test1", "test2"], ids=["测试1", "测试2"])
def test_1(data):
    print(data)

运行结果:
============================= test session starts =============================
collecting ... collected 2 items

test_hook.py::test_1[测试1] PASSED                                       [ 50%]test1

test_hook.py::test_1[测试2] PASSED                                       [100%]test2

============================== 2 passed in 0.01s ==============================

6.2 自定义命令行参数实现环境切换


def pytest_addoption(parser, pluginmanager):

    group = parser.getgroup("JN...自定义参数")
    group.addoption(
        "--env",
        dest="env",
        default="test",
        help="用作切换环境"
    )

@pytest.fixture(scope="session", autouse=True)
def env(request):
    return request.config.getoption("--env")

def test_2(env):
    if env == "dev":
        pass
    else:
        assert 0

=========== 1 passed, 40 deselected, 2 warnings in 0.06s ========

7.1 常用的断言方式

  • 判断是否相等,assert Actual == Expected
  • 判断是否不相等,assert Actual != Expected,其他如 >、

7.2 断言预期异常

import pytest

def division(a, b):
    return a / b

def test_1():
    with pytest.raises(ZeroDivisionError):
        division(1, 2)

def test_2():
    with pytest.raises(ZeroDivisionError):
        division(1, 0)

运行结果:
============================= test session starts =============================
collecting ... collected 2 items

test_assert_exception.py::test_1 FAILED                                  [ 50%]
test_assert_exception.py:7 (test_1)
def test_1():
        with pytest.raises(ZeroDivisionError):
>           division(1, 2)
E           Failed: DID NOT RAISE <class 'ZeroDivisionError'>

test_assert_exception.py:10: Failed

test_assert_exception.py::test_2 PASSED                                  [100%]

========================= 1 failed, 1 passed in 0.02s =========================

Original: https://blog.csdn.net/Augenstern_9512/article/details/126311015
Author: JN…
Title: 测试笔记:学习Pytest框架

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/772151/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球