zoukankan      html  css  js  c++  java
  • pytest测试框架

    1. pytest特点和基本用法

    Python内置了测试框架unit test,但是了解units同学知道它是一个拥有浓烈的Java风格,比如说类名、方法名字会使用驼峰,而且必须要继承父类才能的定义测试用例等等。

    那有一些Python开发者,他觉得这种方式这种风格不太适应,所以做了一个更加pythonic的测试框架,最开始只是工具箱的一部分(py.test),后来这个测试框架独立出来的就成为了大名鼎鼎的pytest。

    1.1 安装pytest

    使用pip进行安装

    pip install pytest -U
    

    验证安装

    pytest
    pytest --version
    pytest -h
    

    1.2 创建测试用例

    1. 创建test_开头的python文件
    2. 编写test_开头的函数
    3. 在函数中使用assert 关键字
    # test_main.py
    
    def test_sanmu():
        a = 1
        b = 2
        assert a == b
    

    1.3 执行测试用例

    • 自动执行所有的用例

      • pytest
    • 执行指定文件中所有用例

      • pytest filename.py
    • 执行指定文件夹中的所有文件中的所有用例

      • pyest dirname
    • 执行指定的用例

      • pytest test_a.py::test_b

    测试发现:搜集用例

    一般规则:

    1. 从当前目录开始,遍历每一个子目录 (不论这个目录是不是包)
    2. 在目录搜索test_*.py*_test.py,并导入(测试文件中的代码会自动执行)
    3. 在导入的模块手机以下特征的对象,作为测试用例
      1. test开头的函数
      2. Test开头类及其test开头方法 (这个类不应该有__init__
      3. unittest框架的测试用例

    惯例(约定):和测试相关的一切,用test或者test_开头

    1.4 读懂测试结果

    import pytest
    
    
    def test_ok():
        print("ok")
    
    
    def test_fail():
        a, b = 1, 2
        assert a == b
    
    
    def test_error(something):
        pass
    
    
    @pytest.mark.xfail(reason="always xfail")
    def test_xpass():
        pass
    
    
    @pytest.mark.xfail(reason="always xfail")
    def test_xfail():
        assert False
    
    
    @pytest.mark.skip(reason="skip is case")
    def test_skip():
        pass
    

    pytest报告分为几个基本部分:

    1. 报告头
    2. 用例收集情况
    3. 执行状态
      1. 用例的结果
      2. 进度
    4. 信息
      1. 错误信息
      2. 统计信息
      3. 耗时信息

    报告中的结果缩写符合是什么含义

    符号 含义
    . 测试通过
    F 测试失败
    E 出错
    X XPass 预期外的通过
    x xfailed 预期失败
    s 跳过用例

    如果要展示更加详细的结果,可以通过参数的方式设置

    pytest -vrA
    

    2. 断言

    2.1 普通断言

    pytest使用python内置关键字assert验证预期值和实际值

    def test_b():
        a = 1
        b = 2
    
        assert a == b
    

    pytest 和python处理方式不一样:

    1. 数值比较:会显示具体数值

    2.2 容器型数据断言

    如果是两个容器型数据(字符串、元组、列表、字典、数组),断言失败,会将两个数据进行diff比较,找出不用

    def test_b():
        a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
        b = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2]
    
        assert a == b, "a和b不相等"
    
    >       assert a == b, "a和b不相等"
    E       AssertionError: a和b不相等
    E       assert [1, 1, 1, 1, 1, 1, ...] == [1, 1, 1, 1, 1, 1, ...]
    E         At index 9 diff: 0 != 2
    E         Full diff:
    E         - [1, 1, 1, 1, 1, 1, 1, 1, 1, 2]
    E         ?                             ^
    E         + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
    E         ?   
    

    2.3 断言出现异常

    一般情况,:

    • 执行测试用例出现了异常,认为失败

    • 如果没有出现异常,认为通过。

    “断言出现异常” :

    • 出现了异常,认为通过
    • 没有出现异常,认为失败
    def test_b():
    
        with pytest.raises(ZeroDivisionError):
            1 / 0
    

    不仅可以断言出现了异常,还可以断言出现什么异常,更可以断言谁引发的异常

    def test_b():
        d = dict()
        with pytest.raises(KeyError) as exc_info:
            print(d["a"])  # 这行代码,预期不发生异常
            print(d["b"])  # 这行代码,预期异常
    
        assert "b" in str(exc_info.value)
    

    2.4 断言出现警告

    警告(Warning)是Exception的子类,但是它不是有raise关键字抛出,而是通过warnings.warn函数进行执行。

    def f():
        pass
        warnings.warn("再过几天,就要放假了", DeprecationWarning)
    
    
    def test_b():
        with pytest.warns(DeprecationWarning):
            f()
    

    3. 夹具

    单元代码?

    创建测试用例:

    1. 创建test_开头的函数
    2. 在函数内使用断言关键字

    一个测试用例的执行分为四个步骤:

    1. 预置条件
    2. 执行动作
    3. 断言结果
    4. 清理现场

    为了重复测试结果不会异常,也为了不会干扰其他用例。

    在理想情况,为了突出用例重点,用例中应该只有2(执行动作)和3(断言结果)

    • 1 和4 应当封装起来
    • 1 和4 能够自动执行

    夹具(Fixture)是软件测试装置,作用和目的:

    • 在测试开始前,准备好相关的测试环境
    • 在测试结束后,销毁相关的内容

    以socket聊天服务器作为例子,演示夹具的用法

    socket服务的测试步骤:

    1. 建立socket连接
    2. 利用socket执行测试动作
    3. 对结果进行断言
    4. 断开socket

    3.1 创建夹具

    3.1.1 快速上手

    夹具的特性:

    1. 在测试用例之前执行
    2. 具体重复性和必要性

    夹具client:自动和server建立socket连接,并供用例使用

    创建一个函数,并使用@pytest.fixture()装饰器即可

    @pytest.fixture()
    def client():
        client = socket.create_connection(server_address, 1)
        return client
    

    3.1.2 setup 和 teardwon

    pytest 有2种方式实现teardwon,这里只推荐一种: 使用yield关键字

    函数中有了yield关键字之后,成了生成器,可以多次调用

    @pytest.fixture()
    def server():
        p = Process(target=run_server, args=(server_address,))
        p.start()  # 启动服务端
        print("启动服务端")
        yield p
        p.kill()
    

    yield关键字 使夹具执行分为2个部分:

    1. yield之前的代码,在测试前执行,对应xUnit中setUP
    2. yield 之后的代码,在测试后执行,对应xUnit中yeadDown

    3.1.3 夹具范围

    夹具生命周期:

    1. 被需要用的时候创建
    2. 在结束范围的时候销毁
    3. 如果夹具存在,不会重复创建

    pytest夹具范围有5种:

    • function:默认的范围,夹具在单个用例结束的时候被销毁
    • class: 夹具在类的最后一个用例结束的时候被销毁
    • module:夹具在模块的最后一个用例结束的时候被销毁
    • package:夹具在包的最后一个用例结束的时候被销毁
    • session:夹具在整个测试活动的最后一个用例结束的时候被销毁

    使用Python,如果完全不会class,是没有任何问题的。

    @pytest.fixture(scope="session")
    def server():
        p = Process(target=run_server, args=(server_address,))
        p.start()  # 启动服务端
        print("启动服务端")
        yield p
        p.kill()
    

    3.1.4 夹具参数化

    夹具的参数,可以通过参数化的方式,为夹具产生多个结果 (产生了多个夹具)

    如果测试用例要使用的夹具被参数化了,那么测试用例得到的夹具结果会有多个,每个夹具都会被使用

    测试用例也会执行多次

    测试用例,不知道自己被执行了多次,正如它不知道夹具被参数一样

    @pytest.fixture(scope="session", params=[9001, 9002, 9003])
    def server(request):
        port = request.param
        p = Process(target=run_server, args=(("127.0.0.1", port),))
        p.start()  # 启动服务端
        print("启动服务端")  # *3
        yield p
        p.kill()
    

    3.2 使用夹具

    3.2.1 在用例中使用

    3.2.2 在夹具中使用

    注意:夹具中使用夹具,必须确保范围是兼容的

    例子:夹具A 和夹具B,A范围是function,B的范围是session,A可以使用B ,B不可用使用A

    • A在第一个用例结束的时候,被销毁
    • B在所有的用例结束的时候,被销毁
    • A比B先被销毁

    使用实际上依赖的关系:

    假设:

    • A使用B
      • B的setup
      • A
      • B的tearDown
    • B使用A (不可以的)
      • 第一个用例结束的时候 A被销毁,B该怎么办?
      • A的setUP
      • B
      • A的tearDown

    生命周期短的夹具,才可用使用声明周期长的夹具

    3.2.4 自动使用夹具

    在一些代码质量工具中,未被使用的变量和参数,会被评为低质量。

    pytest中,夹具可以声明自动执行,不需要写在用例参数列表中了。

    @pytest.fixture(scope="function", autouse=True)
    def server(request):
        port = 9001
        p = Process(target=run_server, args=(("127.0.0.1", port),))
        p.start()  # 启动服务端
        print("启动服务端")  # *3
        yield p
        p.kill()
    

    4. 标记

    默认情况下,pytest执行逻辑:

    1. 运行所有的测试用例
    2. 执行用例的时候,出现异常,判断为测试失败
    3. 执行用例的时候,没有出现异常,判断为测试通过

    标记是给测试用例用的

    标记的作用,就是为了改变默认行为:

    • userfixtures :在测试用例中使用夹具
    • skip:跳过测试用例
    • xfail: 预期失败
    • parametrize: 参数化测试,反复,多次执行测试用例
    • 自定义标记:提供筛选用例的条件,pytest只执行部分用例

    4.1 userfixtures

    @pytest.mark.usefixtures("server",)  # 只能给用例,使用夹具
    class TestSocket:
        def test_create_client(self, client):
            print("客户端的地址", client.getsockname())
            print("服务端的地址", client.getpeername())
    
        def test_send_and_recv(self, client):
            data = "hello world
    "
    
            client.sendall(data.encode())  # 将字符串转为字节,然后发生
    
            f = client.makefile()
    
            msg = f.readline()
    
            assert data == msg
    
    
    def test_():
        pass
    

    4.2 skip 和 skipif

    • skip 无条件跳过
    • skipif 有条件跳过
    class TestSocket:
        @pytest.mark.skip(reason="心情不美丽,不想执行这个测试")
        def test_create_client(self, client):
            print("客户端的地址", client.getsockname())
            print("服务端的地址", client.getpeername())
    
        def test_send_and_recv(self, client):
            data = "hello world
    "
    
            client.sendall(data.encode())  # 将字符串转为字节,然后发生
    
            f = client.makefile()
    
            msg = f.readline()
    
            assert data == msg
    
    class TestSocket:
        @pytest.mark.skipif(sys.platform.startswith("win"), reason="心情不美丽,不想执行这个测试")
        def test_create_client(self, client):
            print("客户端的地址", client.getsockname())
            print("服务端的地址", client.getpeername())
    

    4.3 xfail

    无参数:无条件预期失败

    有参数condition:有条件预期失败

    有参数run: 预期失败的时候,不执行测试用例

    有参数strict:预期外通过时,认为测试失败

    @pytest.mark.xfail(1 != 1, reason="意料中的失败", run=False, strict=True)
    def test_server_not_run():
        """当服务端未启动的时候,客户端应该连接失败"""
    
        my_socket = socket.create_connection(server_address, 1)
    
    

    4.4 参数化

    好处:

    1. 提供测试覆盖率 1,1 => 2, 1,0=>1, 9999999999,1=>100000000
    2. 反复测试,验证测试结果稳定性 1,1 => 2 1,1 => 2 1,1 => 2

    本质:同一个测试代码可以执行多个测试用例

    @pytest.mark.parametrize("n", [1, "x"])
    def test_server_can_re_content(n):
        """测试服务器可以被多个客户端反复连接和断开"""
        print(n)
        my_socket = socket.create_connection(server_address)
    

    4.5 自定义标记

    提供筛选用例的条件,使pytest只执行部分用例

    • 选择简单的标记

      • pytest -m 标记
    • 选择复杂的标记

      • pytest -m "标记A and 标记B" 同时具有标记A 和标记B的用例
      • pytest -m "标记A or 标记B" 具有标记A 或标记B 的用例
      • pytest -m "not 标记A " 不具有标记A 的B用例
    @pytest.mark.mmm
    @pytest.mark.sanmu
    def test_sanmu():
        pass
    
    
    @pytest.mark.mmm
    @pytest.mark.yiran
    def test_yiran():
        pass
    

    注册自定义标记:pytest知道哪些自定义标记是正确的,就不会发出警告

    # pytest.ini
    [pytest]
    markers =
     mmm
     sanmu
     yiran
    

    5. 配置

    5.1 配置方法

    1. 命令行
      • 灵活
      • 如果有多个选项的话,不方便
    2. 配置文件
      • 特别适合大量,或者不常修改的选项
      • pytest.ini
      • pyproject.toml
        • pytest 6.0+ 支持
        • 是PEP标准
        • 是未来
    3. python代码动态配置
      • 太灵活, 意味着容易出错
      • 优先级是最高的
    # conftest.py 会被pytest自动加载,适合写配置信息
    
    def pytest_configure(config):  # 钩子:pytest会自动发现并运行这个函数
    
        config.addinivalue_line("markers", "mmm")
        config.addinivalue_line("markers", "sanmu")
        config.addinivalue_line("markers", "yiran")
    

    5.2 配置项

    1. 查询帮助信息 pytest -h
    2. 查看pytest参考文档 https://docs.pytest.org/en/stable/reference.html#id90

    约定大于配置

    6. 插件

    一般情况,插件是一个python的包,在pypi,使用pytest-开头

    不一般的情况,需要把插件的在confgtest.py进行启用

    6.1 安装插件

    pip install pytest-html
    pip install pytest-httpx  # mock httpx
    pip install pytest-django  # test  django
    

    6.2 使用插件

    各个插件的使用方法 ,各不相同

    参考各插件自己的问题

    有些插件时自动启用的,不需要任何操作

    6.3 禁用插件

    添加参数

    pytest -p no:插件名称
    
    • 包名称:pytest-html
    • 插件名称 :html

    7. 布局

    特性:

    1. 如果一个测试文件,存放在目录中,那么执行时,这个目录成为顶级目录
    2. 如果一个测试文件,存放在包中,那么执行时,根目录成为顶级目录
    3. python -m pytest ,将当前目录加入到sys.path ,当前目录中的模块可以被导入
  • 相关阅读:
    27. Remove Element
    列表变成字典
    1. Two Sum
    CVPR2019:What and How Well You Performed? A Multitask Learning Approach to Action Quality Assessment
    959. Regions Cut By Slashes
    118. Pascal's Triangle
    loj3117 IOI2017 接线 wiring 题解
    题解 NOI2019 序列
    题解 省选联考2020 组合数问题
    题解 Educational Codeforces Round 90 (Rated for Div. 2) (CF1373)
  • 原文地址:https://www.cnblogs.com/adnny/p/15314867.html
Copyright © 2011-2022 走看看