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 ,当前目录中的模块可以被导入
  • 相关阅读:
    xheditor编辑器自动上传外链图片及QQ截图等(升级支持webp格式)
    jQuery对checkbox的各种操作
    Jquery过滤选择器,选择前几个元素,后几个元素,内容过滤选择器等
    Java8新特性之forEach+Lambda 表达式遍历Map和List
    My97日期控件My97 DatePicker选择每月最后一天(周6周日不能选,节假日不能选,高亮每个月最后一个股票交易日)
    算法系列:日历算法
    mysql 将多个查询结果合并成一行
    js判断浏览器类型以及语言
    升级至 spring-5.3.0 关于 jdbcTemplate.query(sql, parameters, rowMapper) 的解决
    解决 redis Increased maximum number of open files to 10032 (it was originally set to 256).
  • 原文地址:https://www.cnblogs.com/adnny/p/15314867.html
Copyright © 2011-2022 走看看