单元测试:Testing leads to failure, and failure leads to understanding

这篇具有很好参考价值的文章主要介绍了单元测试:Testing leads to failure, and failure leads to understanding。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

单元测试的概念可能多数读者都有接触过。作为开发人员,我们编写一个个测试用例,测试框架发现这些测试用例,将它们组装成测试 suite 并运行,收集测试报告,并且提供测试基础设施(断言、mock、setup 和 teardown 等)。Python 当中最主流的单元测试框架有三种,Pytest, nose 和 Unittest,其中 Unittest 是标准库,其它两种是第三方工具。在 ppw 向导生成的项目中,就使用了 Pytest 来驱动测试。

这里主要比较一下 pytest 和 unittest。多数情况下,当我们选择单元测试框架时,选择二者之一就好了。unitttest 基于类来组织测试用例,而 pytest 则是函数式的,基于模块来组织测试用例,同时它也提供了 group 概念来组织测试用例。pytest 的 mock 是基于第三方的 pytest-mock,而 pytest-mock 实际上只是对标准库中的 mock 的简单封装。单元测试都会有 setup 和 teardown 的概念,unittest 直接使用了 setUp 和 tearDown 作为测试入口和结束的 API,在 pytest 中,则是通过 fixture 来实现,这方面学习曲线可能稍微陡峭一点。在断言方面,pytest 使用 python 的关键字 assert 进行断言,比 unittest 更为简洁,不过断言类型上没有 unittest 丰富。

另外一个值得一提的区别是,unittest 从 python 3.8 起就内在地支持 asyncio,而在 pytest 中,则需要插件 pytest-asyncio 来支持。但两者在测试的兼容性上并没有大的不同。

pytest 的主要优势是有:

  1. pytest 的测试用例更简洁。由于测试用例并不是正式代码,开发者当然希望少花时间在这些代码上,因此代码的简洁程度很重要。
  2. 提供了命令行工具。如果我们仅使用 unittest,则执行单元测试必须要使用python -m unittest来执行;而通过 pytest 来执行单元测试,我们只需要调用pytest .即可。
  3. pytest 提供了 marker,可以更方便地决定哪些用例执行或者不执行。
  4. pytest 提供了参数化测试。

这里我们简要地举例说明一下什么是参数化测试,以便读者理解为什么参数化测试是一个值得一提的优点。

# 示例 7 - 1
import pytest
from datetime import datetime
from src.example import get_time_of_day

@pytest.mark.parametrize(
    "datetime_obj, expect",
    [
        (datetime(2016, 5, 20, 0, 0, 0), "Night"),
        (datetime(2016, 5, 20, 1, 10, 0), "Night"),
        (datetime(2016, 5, 20, 6, 10, 0), "Morning"),
        (datetime(2016, 5, 20, 12, 0, 0), "Afternoon"),
        (datetime(2016, 5, 20, 14, 10, 0), "Afternoon"),
        (datetime(2016, 5, 20, 18, 0, 0), "Evening"),
        (datetime(2016, 5, 20, 19, 10, 0), "Evening"),
    ],
)
def test_get_time_of_day(datetime_obj, expect, mocker):
    mock_now = mocker.patch("src.example.datetime")
    mock_now.now.return_value = datetime_obj

    assert get_time_of_day() == expect​

在这个示例中,我们希望用不同的时间参数,来测试 get_time_of_day 这个方法。如果使用 unittest,我们需要写一个循环,依次调用 get_time_of_day(),然后对比结果。而在 pytest 中,我们只需要使用 parametrize 这个注解,就可以传入参数数组(包括期望的结果),进行多次测试,不仅代码量要少不少,更重要的是,这种写法更加清晰。

基于以上原因,在后面的内容中,我们将以 pytest 为例进行介绍。

1. 测试代码的组织

我们一般将所有的测试代码都归类在项目根目录下的 tests 文件夹中。每个测试文件的名字,要么使用 test_.py,要么使用_test.py。这是测试框架的要求。如此以来,当我们执行命令如pytest tests时,测试框架就能从这些文件中发现测试用例,并组合成一个个待执行的 suite。

在 test_*.py 中,函数名一样要遵循一定的模式,比如使用 test_xxx。不遵循规则的测试函数,不会被执行。

一般来说,测试文件应该与功能模块文件一一对应。如果被测代码有多重文件夹,对应的测试代码也应该按同样的目录来组织。这样做的目的,是为了将商业逻辑与其测试代码对应起来,方便我们添加新的测试用例和对测试用例进行重构。

比如在 ppw 生成的示例工程中,我们有:


sample
├── sample
│   ├── __init__.py
│   ├── app.py
│   └── cli.py
├── tests
│   ├── __init__.py
│   ├── test_app.py
│   └── test_cli.py

注意这里面的__init__.py 文件,如果缺少这个文件的话,tests 就不会成为一个合法的包,从而导致 pytest 无法正确导入测试用例。

2. PYTEST

使用 pytest 写测试用例很简单。假设 sample\app.py 如下所示:

# 示例 7 - 2
def inc(x:int)->int:
    return x + 1

则我们的 test_app.py 只需要有以下代码即可完成测试:

# 示例 7 - 3
import pytest
from sample.app import inc

def test_inc():
    assert inc(3) == 4

这比 unittest 下的代码要简洁很多。

2.1. 测试用例的组装

在 pytest 中,pytest 会按传入的文件(或者文件夹),搜索其中的测试用例并组装成测试集合 (suite)。除此之外,它还能通过 pytest.mark 来标记哪些测试用例是需要执行的,哪些测试用例是需要跳过的。

# 示例 7 - 4
import pytest

@pytest.mark.webtest
def test_send_http():
    pass  # perform some webtest test for your app

def test_something_quick():
    pass

def test_another():
    pass

class TestClass:
    def test_method(self):
        pass

然后我们就可以选择只执行标记为 webtest 的测试用例:

$ pytest -v -m webtest

=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 3 deselected / 1 selected

test_server.py::test_send_http PASSED                                [100%]

===================== 1 passed, 3 deselected in 0.12s ======================

从输出可以看出,只有 test_send_http 被执行了。

这里的 webtest 是自定义的标记。pytest 还内置了这些标记,有的也可以用来筛选用例:

  1. pytest.mark.filterwarnings, 给测试用例添加 filterwarnings 标记,可以忽略警告信息。
  2. pytest.mark.skip,给测试用例添加 skip 标记,可以跳过测试用例。
  3. pytest.mark.skipif, 给测试用例添加 skipif 标记,可以根据条件跳过测试用例。
  4. pytest.mark.xfail, 在某些条件下(比如运行在某个 os 上),用例本应该失败,此时就应使用此标记,以便在测试报告中标记出来。
  5. pytest.mark.parametrize, 给测试用例添加参数化标记,可以根据参数化的参数执行多次测试用例。

这些标记可以用 pytest --markers 命令查看。

2.2. pytest 断言

在测试时,当我们调用一个方法之后,会希望将其返回结果与期望结果进行比较,以决定该测试是否通过。这被称之为测试断言。

pytest 中的断言巧妙地拦截并复用了 python 内置的函数 assert,由于您很可能已经接触过 assert 了,因而使得这一部分的学习成本变得非常低。

# 示例 7 - 5
def test_assertion():
    # 判断基本变量相等
    assert "loud noises".upper() == "LOUD NOISES"

    # 判断列表相等
    assert [1, 2, 3] == list((1, 2, 3))

    # 判断集合相等
    assert set([1, 2, 3]) == {1, 3, 2}

    # 判断字典相等
    assert dict({
        "one": 1,
        "two": 2
    }) == {
        "one": 1,
        "two": 2
    }

    # 判断浮点数相等
    # 缺省地, ORIGIN  ± 1E-06
    assert 2.2 == pytest.approx(2.2 + 1e-6)
    assert 2.2 == pytest.approx(2.3, 0.1)

    # 如果要判断两个浮点数组是否相等,我们需要借助 NUMPY.TESTING
    import numpy

    arr1 = numpy.array([1., 2., 3.])
    arr2 = arr1 + 1e-6
    numpy.testing.assert_array_almost_equal(arr1, arr2)

    # 异常断言:有些用例要求能抛出异常
    with pytest.raises(ValueError) as e:
        raise ValueError("some error")
    
    msg = e.value.args[0]
    assert msg == "some error"

上面的代码分别演示了如何判断内置类型、列表、集合、字典、浮点数和浮点数组是否相等。这部分语法跟标准 python 语法并无二致。pytest 与 unittest 一样,都没有提供如何判断两个浮点数数组是否相等的断言,如果有这个需求,我们可以求助于 numpy.testing,正如例子中第 25~30 行所示。

有时候我们需要测试错误处理,看函数是否正确地抛出了异常,代码 32~37 演示了异常断言的使用。注意这里我们不应该这么写:

# 示例 7 - 6
    try:
        # CALL SOME_FUNC WILL RAISE VALUEERROR
    except ValueError as e:
        assert str(e) == "some error":
    else:
        assert False

上述代码看上去逻辑正确,但它混淆了异常处理和断言,使得他人一时难以分清这段代码究竟是在处理测试代码中的异常呢,还是在测试被调用函数能否正确抛出异常,明显不如异常断言那样清晰。

2.3. pytest fixture

一般而言,我们的测试用例很可能需要依赖于一些外部资源,比如数据库、缓存、第三方微服务等。这些外部资源的初始化和销毁,我们希望能够在测试用例执行前后自动完成,即自动完成 setup 和 teardown 的操作。这时候,我们就需要用到 pytest 的 fixture。

单元测试:Testing leads to failure, and failure leads to understanding,Python能做大项目,单元测试,python

假定我们有一个测试用例,它需要连接数据库,代码如下(参见 code/chap07/sample/app.py)

# 示例 7 - 7
import asyncpg
import datetime

async def add_user(conn: asyncpg.Connection, name: str, date_of_birth: datetime.date)->int:
    # INSERT A RECORD INTO THE CREATED TABLE.
    await conn.execute('''
        INSERT INTO users(name, dob) VALUES($1, $2)
    ''', name, date_of_birth)

    # SELECT A ROW FROM THE TABLE.
    row: asyncpg.Record = await conn.fetchrow(
        'SELECT * FROM users WHERE name = $1', 'Bob')
    # *ROW* NOW CONTAINS
    # ASYNCPG.RECORD(ID=1, NAME='BOB', DOB=DATETIME.DATE(1984, 3, 1))
    return row["id"]

我们先展示测试代码(参见 code/chap07/sample/test_app.py),再结合代码讲解 fixture 的使用:

# 示例 7 - 8
import pytest
from sample.app import add_user
import pytest_asyncio
import asyncio

# PYTEST-ASYNCIO 已经提供了一个 EVENT_LOOP 的 FIXTURE, 但它是 FUNCTION 级别的
# 这里我们需要一个 SESSION 级别的 FIXTURE,所以我们需要重新实现
@pytest.fixture(scope="session")
def event_loop():
    policy = asyncio.get_event_loop_policy()
    loop = policy.new_event_loop()
    yield loop
    loop.close()

@pytest_asyncio.fixture(scope='session')
async def db():
    import asyncpg
    conn = await asyncpg.connect('postgresql://zillionare:123456@localhost/bpp')
    yield conn

    await conn.close()

@pytest.mark.asyncio
async def test_add_user(db):
    import datetime
    user_id = await add_user(db, 'Bob', datetime.date(2022, 1, 1))
    assert user_id == 1

我们的功能代码很简单,就是往 users 表里插入一条记录,并返回它在表中的 id。测试代码调用 add_user 这个函数,然后检测返回值是否为 1(如果每次测试前都新建数据库或者清空表的话,那么返回的 ID 就应该是 1)。

这个测试显然需要连接数据库,因此我们需要在测试前创建一个数据库连接,然后在测试结束后关闭连接。并且,我们还会有多个测试用例需要连接数据库,因此我们希望数据库连接是一个全局的资源,可以在多个测试用例中共享。这就是 fixture 的用武之地。

fixture 是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们。但与 unitest 中的 setup 和 teardown 不同,pytest 中的 fixture 依赖是显式声明的。比如,在上面的 test_add_user 显式依赖了 db 这个 fixture(通过在函数声明中传入 db 作为参数),而 db 则又显示依赖 event_loop 这个 fixture。即使文件中还存在其它 fixture, test_add_user 也不会依赖到这些 fixture,因为依赖必须显式声明。

上面的代码中,我们演示的是对异步函数 add_user 的测试。显然,异步函数必须在某个 event loop 中执行,并且相关的初始化 (setup) 和退出操作 (teardown) 也必须在同一个 loop 中执行。这里是分别通过 pytest.mark.asyncio, pytest_asyncio 等 fixture 来实现的:

首先,我们需要将测试用例标注为异步执行,即上面的代码第 21 行。其次,test_add_user 需要一个数据库连接,该连接由 fixture db来提供。这个连接的获得也是异步的,因此,我们不能使用 pytest.fixutre 来声明该函数,而必须使用@pytest_asyncio.fixture 来声明该函数。

最后,我们还必须提供一个 event_loop 的 fixture,它是一切的关键。当某个函数被 pytest.mark.asyncio 装饰时,该函数将在 event_loop 提供的 event loop 中执行。

我们还要介绍一下出现在第 6 行和第 13 行中的 scope=‘session’。这个参数表示 fixture 的作用域,它有四个可选值:function, class, module 和 session。默认值是 function,表示 fixture 只在当前测试函数中有效。在上面的示例中,我们希望这个 event loop 在一次测试中都有效,所以将 scope 设置为 session。

上面的例子是关于异步模式下的测试的。对普通函数的测试更简单一些。我们不需要 pytest.mark.asynio 这个装饰器,也不需要 event_loop 这个 fixture。所有的 pytest_asyncio.fixture 都换成 pytest.fixture 即可(显然,它必须、也只能装饰普通函数,而非由 async 定义的函数)。

单元测试:Testing leads to failure, and failure leads to understanding,Python能做大项目,单元测试,python

我们通过上面的例子演示了 fixture。与 markers 类似,要想知道我们的测试环境中存在哪些 fixtures,可以通过 pytest --fixtures 来显示当前环境中所有的 fixture。

$ pytest --fixtures

------------- fixtures defined from faker.contrib.pytest.plugin --------------
faker -- .../faker/contrib/pytest/plugin.py:24
    Fixture that returns a seeded and suitable ``Faker`` instance.

------------- fixtures defined from pytest_asyncio.plugin -----------------
event_loop -- .../pytest_asyncio/plugin.py:511
    Create an instance of the default event loop for each test case.

...

------------- fixtures defined from tests.test_app ----------------
event_loop [session scope] -- tests/test_app.py:45

db [session scope] -- tests/test_app.py:52

这里我们看到 faker.contrib 提供了一个名为 faker 的 fixture, 我们之前安装的、支持异步测试的 pytest_asyncio 也提供了名为 event_loop 的 fixture(为节省篇幅,其它几个省略了),以及我们自己测试代码中定义的 event_loop 和 db 这两个 fixture。

Pytest 还提供了一类特别的 fixture,即 pytest-mock。为了讲解方便,我们先安装 pytest-mock 这个插件,看看它提供的 fixture。

$ pip install pytest-mock
pytest --fixture

------- fixtures defined from pytest_mock.plugin --------
class_mocker [class scope] -- .../pytest_mock/plugin.py:419
    Return an object that has the same interface to the `mock` module, but
    takes care of automatically undoing all patches after each test method.

mocker -- .../pytest_mock/plugin.py:419
    Return an object that has the same interface to the `mock` module, but
    takes care of automatically undoing all patches after each test method.

module_mocker [module scope] -- .../pytest_mock/plugin.py:419
    Return an object that has the same interface to the `mock` module, but
    takes care of automatically undoing all patches after each test method.

package_mocker [package scope] -- .../pytest_mock/plugin.py:419
    Return an object that has the same interface to the `mock` module, but
    takes care of automatically undoing all patches after each test method.

session_mocker [session scope] -- .../pytest_mock/plugin.py:419
    Return an object that has the same interface to the `mock` module, but
    takes care of automatically undoing all patches after each test method.

可以看到 pytest-mock 提供了 5 个不同级别的 fixture。关于什么是 mock,这是下一节的内容。


本文摘自《Python能做大项目》,将由机械工业出版社出版。全书已发表在大富翁量化上,欢迎提前阅读!文章来源地址https://www.toymoban.com/news/detail-796674.html

到了这里,关于单元测试:Testing leads to failure, and failure leads to understanding的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Testing】基于Mockito的FeignClient单元测试

           在微服务框架模式下,往往需要在外部服务没有准备好的情况下进行测试。本文主要是讨论在SpringCloud体系下,以FeignClient进行通信时,对其进行mock的方式。 微服务B通过FeignClient依赖其他服务调用。 3.1)构建基础测试类 3.2)Mock FeignClient 主要就是初始化feignClient的方式(

    2024年02月12日
    浏览(26)
  • 【测试开发】单元测试、基准测试和性能分析(以 Go testing 为例)

    你写不出 bug-free 的代码。 你认为自己写出了 bug-free 的代码,但它在你意想不到的地方出错了。 你觉得自己写出了永不出错的代码,但它的性能十分糟糕。 “测试左移”距离真正落地或许还有不短的距离,但在开发过程中注重自己的代码质量,至少养成 写单测 的习惯还是很

    2024年02月04日
    浏览(39)
  • Go 单元测试中 testing 包的数据类型M/T/B/PB

    testing.M 对main方法进行的测试 testing.T 对函数/方法进行单元测试 testing. B 对性能进行的测试 testing.PB - 命令 作用 go test 【包名】或 go test . 运行当前package内的所有用例 go test ./… 或 go test 【目录名】/… 递归执行当前目录下所有用例: go test -v [单元测试文件]. // 如 go test -v f

    2024年04月17日
    浏览(18)
  • Tests run: 0, Failures: 0, Errors: 0, Skipped: 0【Junit4 升级 Junit5】【Maven 检测不到单元测试问题】

    由于测试容器,有残留的 JUnit4 依赖项。能够通过显式将 JUnit5 设置为万无一失插件的依赖项来解决此问题,如下所示:

    2024年04月27日
    浏览(30)
  • diffusers-Understanding models and schedulers

    https://huggingface.co/docs/diffusers/using-diffusers/write_own_pipeline https://huggingface.co/docs/diffusers/using-diffusers/write_own_pipeline diffusers有3个模块:diffusion pipelines,noise schedulers,model。这个库很不错,设计思想和mmlab系列的有的一拼,mm系列生成算法在mmagic中,但是不如diffusers丰富,再者几乎

    2024年02月06日
    浏览(39)
  • Visualizing and Understanding Convolutional Networks阅读笔记

       CNN模型已经取得了非常好的效果,但是在大多数人眼中,只是一个“黑盒”模型,目前还不清楚 为什么它们表现得如此好,也不清楚如何改进 。在本文中,我们探讨了这两个问题。我们介绍了一种新的可视化技术,可以深入了解中间特征层的功能和分类器的操作。通过

    2024年02月12日
    浏览(25)
  • System Under Test (SUT) and Testing Environment

    1. System Under Test (SUT) The System Under Test (SUT) is the specific software application, component, or system that is being subjected to testing. It is the main focus of the testing process and represents the software or system that developers and testers are working on. The SUT could be a single software application, a module within a larger system, or

    2024年02月12日
    浏览(26)
  • IPQ6010: Leading a new chapter in smart home and IoT fields

    With the rapid development of smart home and Internet of Things technology, the demand for high-performance processors is also growing. The IPQ6010 system-on-chip launched by Qualcomm Technologies is designed to meet this demand. It has become a leader in smart home and IoT with its powerful processing performance, excellent graphics performance and high-spe

    2024年01月22日
    浏览(33)
  • Testing Angular, VueJS, and React Components with Jest

    作者:禅与计算机程序设计艺术 在过去的几年里,React、Angular和Vue等前端框架都获得了越来越多开发者的青睐,并且取得了不俗的成绩。这些前端框架的出现给前端开发领域带来了许多新鲜的机会。特别是在面对复杂业务需求时,测试驱动开发(TDD)方法对于保证项目质量至

    2024年02月06日
    浏览(32)
  • Java Test: Specification and Structure Testing(line, branch, path coverage)

    这篇文章梳理一下Java软件测试中的Secification test和Structure test。 规范测试(specification test):又称黑盒测试(black-box testing)或需求驱动测试(requirements-driven testing),这种测试方法关注程序的功能和性能,而不关注其内部实现。 Specification(规范)是对软件组件、系统或方法

    2024年02月02日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包