首页 > 其他分享 >PyTest-快速启动指南(二)

PyTest-快速启动指南(二)

时间:2024-04-17 13:45:29浏览次数:24  
标签:指南 启动 self fixture pytest PyTest 测试 test def

PyTest 快速启动指南(二)

原文:zh.annas-archive.org/md5/ef4cd099dd041b2b3c7ad8b8d5fa4114

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:fixtures

在上一章中,我们学习了如何有效地使用标记和参数化来跳过测试,将其标记为预期失败,并对其进行参数化,以避免重复。

现实世界中的测试通常需要创建资源或数据来进行操作:一个临时目录来输出一些文件,一个数据库连接来测试应用程序的 I/O 层,一个用于集成测试的 Web 服务器。这些都是更复杂的测试场景中所需的资源的例子。更复杂的资源通常需要在测试会话结束时进行清理:删除临时目录,清理并断开与数据库的连接,关闭 Web 服务器。此外,这些资源应该很容易地在测试之间共享,因为在测试过程中我们经常需要为不同的测试场景重用资源。一些资源创建成本很高,但因为它们是不可变的或者可以恢复到原始状态,所以应该只创建一次,并与需要它的所有测试共享,在最后一个需要它们的测试完成时销毁。

pytest 最重要的功能之一是覆盖所有先前的要求和更多内容。

本章我们将涵盖以下内容:

  • 引入 fixtures

  • 使用conftest.py文件共享 fixtures

  • 作用域

  • 自动使用

  • 参数化

  • 使用 fixtures 中的标记

  • 内置 fixtures 概述

  • 提示/讨论

引入 fixtures

大多数测试需要某种数据或资源来操作:

def test_highest_rated():
    series = [
        ("The Office", 2005, 8.8),
        ("Scrubs", 2001, 8.4),
        ("IT Crowd", 2006, 8.5),
        ("Parks and Recreation", 2009, 8.6),
        ("Seinfeld", 1989, 8.9),
    ]
    assert highest_rated(series) == "Seinfeld"

这里,我们有一个(series name, year, rating)元组的列表,我们用它来测试highest_rated函数。在这里将数据内联到测试代码中对于孤立的测试效果很好,但通常你会有一个可以被多个测试使用的数据集。一种解决方法是将数据集复制到每个测试中:

def test_highest_rated():
    series = [
        ("The Office", 2005, 8.8),
        ...,
    ]
    assert highest_rated(series) == "Seinfeld"

def test_oldest():
    series = [
        ("The Office", 2005, 8.8),
        ...,
    ]
    assert oldest(series) == "Seinfeld"

但这很快就会变得老套—此外,复制和粘贴东西会在长期内影响可维护性,例如,如果数据布局发生变化(例如,添加一个新项目到元组或演员阵容大小)。

进入 fixtures

pytest 对这个问题的解决方案是 fixtures。fixtures 用于提供测试所需的函数和方法。

它们是使用普通的 Python 函数和@pytest.fixture装饰器创建的:

@pytest.fixture
def comedy_series():
    return [
        ("The Office", 2005, 8.8),
        ("Scrubs", 2001, 8.4),
        ("IT Crowd", 2006, 8.5),
        ("Parks and Recreation", 2009, 8.6),
        ("Seinfeld", 1989, 8.9),
    ]

在这里,我们创建了一个名为comedy_series的 fixture,它返回我们在上一节中使用的相同列表。

测试可以通过在其参数列表中声明 fixture 名称来访问 fixtures。然后测试函数会接收 fixture 函数的返回值作为参数。这里是comedy_series fixture 的使用:

def test_highest_rated(comedy_series):
    assert highest_rated(comedy_series) == "Seinfeld"

def test_oldest(comedy_series):
    assert oldest(comedy_series) == "Seinfeld"

事情是这样的:

  • pytest 在调用测试函数之前查看测试函数的参数。这里,我们有一个参数:comedy_series

  • 对于每个参数,pytest 获取相同名称的 fixture 函数并执行它。

  • 每个 fixture 函数的返回值成为一个命名参数,并调用测试函数。

请注意,test_highest_ratedtest_oldest各自获得喜剧系列列表的副本,因此如果它们在测试中更改列表,它们不会相互干扰。

还可以使用方法在类中创建 fixtures:

class Test:

    @pytest.fixture
    def drama_series(self):
        return [
            ("The Mentalist", 2008, 8.1),
            ("Game of Thrones", 2011, 9.5),
            ("The Newsroom", 2012, 8.6),
            ("Cosmos", 1980, 9.3),
        ]

在测试类中定义的 fixtures 只能被类或子类的测试方法访问:

class Test:
    ...

    def test_highest_rated(self, drama_series):
        assert highest_rated(drama_series) == "Game of Thrones"

    def test_oldest(self, drama_series):
        assert oldest(drama_series) == "Cosmos"

请注意,测试类可能有其他非测试方法,就像任何其他类一样。

设置/拆卸

正如我们在介绍中看到的,测试中使用的资源通常需要在测试完成后进行某种清理。

在我们之前的例子中,我们有一个非常小的数据集,所以在 fixture 中内联它是可以的。然而,假设我们有一个更大的数据集(比如,1000 个条目),那么在代码中写入它会影响可读性。通常,数据集在外部文件中,例如 CSV 格式,因此将其移植到 Python 代码中是一件痛苦的事情。

解决方法是将包含系列数据集的 CSV 文件提交到存储库中,并在测试中使用内置的csv模块进行读取;有关更多详细信息,请访问docs.python.org/3/library/csv.html

我们可以更改comedy_series fixture 来实现这一点:

@pytest.fixture
def comedy_series():
    file = open("series.csv", "r", newline="")
    return list(csv.reader(file))

这样做是有效的,但是我们作为认真的开发人员,希望能够正确关闭该文件。我们如何使用 fixtures 做到这一点呢?

Fixture 清理通常被称为teardown,并且可以使用yield语句轻松支持:

@pytest.fixture
def some_fixture():
    value = setup_value()
    yield value
    teardown_value(value)

通过使用yield而不是return,会发生以下情况:

  • fixture 函数被调用

  • 它执行直到 yield 语句,其中暂停并产生 fixture 值

  • 测试执行,接收 fixture 值作为参数

  • 无论测试是否通过,函数都会恢复执行,以执行其清理操作

对于熟悉它的人来说,这与上下文管理器docs.python.org/3/library/contextlib.html#contextlib.contextmanager)非常相似,只是您不需要用 try/except 子句将 yield 语句包围起来,以确保在发生异常时仍执行 yield 后的代码块。

让我们回到我们的例子;现在我们可以使用yield而不是return并关闭文件:

@pytest.fixture
def comedy_series():
    file = open("series.csv", "r", newline="")
    yield list(csv.reader(file))
    file.close()

这很好,但请注意,因为yield与文件对象的with语句配合得很好,我们可以这样写:

@pytest.fixture
def comedy_series():
    with open("series.csv", "r", newline="") as file:
        return list(csv.reader(file))

测试完成后,with语句会自动关闭文件,这更短,被认为更符合 Python 风格。

太棒了。

可组合性

假设我们收到一个新的 series.csv 文件,其中包含更多的电视系列,包括以前的喜剧系列和许多其他类型。我们希望为一些其他测试使用这些新数据,但我们希望保持现有的测试与以前一样工作。

在 pytest 中,fixture 可以通过声明它们为参数轻松依赖于其他 fixtures。利用这一特性,我们能够创建一个新的 series fixture,从series.csv中读取所有数据(现在包含更多类型),并将我们的comedy_series fixture 更改为仅过滤出喜剧系列:

@pytest.fixture
def series():
    with open("series.csv", "r", newline="") as file:
        return list(csv.reader(file))

@pytest.fixture
def comedy_series(series):
    return [x for x in series if x[GENRE] == "comedy"]

使用comedy_series的测试保持不变:

def test_highest_rated(comedy_series):
    assert highest_rated(comedy_series) == "Seinfeld"

def test_oldest(comedy_series):
    assert oldest(comedy_series) == "Seinfeld"

请注意,由于这些特性,fixtures 是依赖注入的一个典型例子,这是一种技术,其中函数或对象声明其依赖关系,但否则不知道或不关心这些依赖关系将如何创建,或者由谁创建。这使它们非常模块化和可重用。

使用 conftest.py 文件共享 fixtures

假设我们需要在其他测试模块中使用前一节中的comedy_series fixture。在 pytest 中,通过将 fixture 代码移动到conftest.py文件中,可以轻松共享 fixtures。

conftest.py文件是一个普通的 Python 模块,只是它会被 pytest 自动加载,并且其中定义的任何 fixtures 都会自动对同一目录及以下的测试模块可用。考虑一下这个测试模块的层次结构:

tests/
    ratings/
        series.csv
        test_ranking.py
    io/
        conftest.py
        test_formats.py 
    conftest.py

tests/conftest.py文件位于层次结构的根目录,因此在该项目中,任何在其中定义的 fixtures 都会自动对所有其他测试模块可用。在tests/io/conftest.py中定义的 fixtures 将仅对tests/io及以下模块可用,因此目前仅对test_formats.py可用。

这可能看起来不像什么大不了的事,但它使共享 fixtures 变得轻而易举:当编写测试模块时,能够从小处开始使用一些 fixtures,知道如果将来这些 fixtures 对其他测试有用,只需将 fixtures 移动到conftest.py中即可。这避免了复制和粘贴测试数据的诱惑,或者花费太多时间考虑如何从一开始组织测试支持代码,以避免以后进行大量重构。

作用域

夹具总是在测试函数请求它们时创建的,通过在参数列表上声明它们,就像我们已经看到的那样。默认情况下,每个夹具在每个测试完成时都会被销毁。

正如本章开头提到的,一些夹具可能很昂贵,需要创建或设置,因此尽可能少地创建实例将非常有帮助,以节省时间。以下是一些示例:

  • 初始化数据库表

  • 例如,从磁盘读取缓存数据,大型 CSV 数据

  • 启动外部服务

为了解决这个问题,pytest 中的夹具可以具有不同的范围。夹具的范围定义了夹具应该在何时清理。在夹具没有清理的情况下,请求夹具的测试将收到相同的夹具值。

@pytest.fixture装饰器的范围参数用于设置夹具的范围:

@pytest.fixture(scope="session")
def db_connection():
    ...

以下范围可用:

  • scope="session":当所有测试完成时,夹具被拆除。

  • scope="module":当模块的最后一个测试函数完成时,夹具被拆除。

  • scope="class":当类的最后一个测试方法完成时,夹具被拆除。

  • scope="function":当请求它的测试函数完成时,夹具被拆除。这是默认值。

重要的是要强调,无论范围如何,每个夹具都只会在测试函数需要它时才会被创建。例如,会话范围的夹具不一定会在会话开始时创建,而是只有在第一个请求它的测试即将被调用时才会创建。当考虑到并非所有测试都可能需要会话范围的夹具,并且有各种形式只运行一部分测试时,这是有意义的,正如我们在前几章中所看到的。

范围的作用

为了展示作用域,让我们看一下在测试涉及某种数据库时使用的常见模式。在即将到来的示例中,不要关注数据库 API(无论如何都是虚构的),而是关注涉及的夹具的概念和设计。

通常,连接到数据库和表的创建都很慢。如果数据库支持事务,即执行可以原子地应用或丢弃的一组更改的能力,那么可以使用以下模式。

首先,我们可以使用会话范围的夹具连接和初始化我们需要的表的数据库:

@pytest.fixture(scope="session")
def db():
    db = connect_to_db("localhost", "test") 
    db.create_table(Series)
    db.create_table(Actors)
    yield db
    db.prune()
    db.disconnect()

请注意,我们会在夹具结束时修剪测试数据库并断开与其的连接,这将在会话结束时发生。

通过db夹具,我们可以在所有测试中共享相同的数据库。这很棒,因为它节省了时间。但它也有一个缺点,现在测试可以更改数据库并影响其他测试。为了解决这个问题,我们创建了一个事务夹具,在测试开始之前启动一个新的事务,并在测试完成时回滚事务,确保数据库返回到其先前的状态:

@pytest.fixture(scope="function")
def transaction(db):
    transaction = db.start_transaction()
    yield transaction
    transaction.rollback()

请注意,我们的事务夹具依赖于db。现在测试可以使用事务夹具随意读写数据库,而不必担心为其他测试清理它:

def test_insert(transaction):
    transaction.add(Series("The Office", 2005, 8.8))
    assert transaction.find(name="The Office") is not None

有了这两个夹具,我们就有了一个非常坚实的基础来编写我们的数据库测试:需要事务夹具的第一个测试将通过db夹具自动初始化数据库,并且从现在开始,每个需要执行事务的测试都将从一个原始的数据库中执行。

不同范围夹具之间的可组合性非常强大,并且使得在现实世界的测试套件中可以实现各种巧妙的设计。

自动使用

可以通过将autouse=True传递给@pytest.fixture装饰器,将夹具应用于层次结构中的所有测试,即使测试没有明确请求夹具。当我们需要在每个测试之前和/或之后无条件地应用副作用时,这是有用的。

@pytest.fixture(autouse=True)
def setup_dev_environment():
    previous = os.environ.get('APP_ENV', '')
    os.environ['APP_ENV'] = 'TESTING'
    yield
    os.environ['APP_ENV'] = previous

自动使用的夹具适用于夹具可供使用的所有测试:

  • 与夹具相同的模块

  • 在方法定义的情况下,与装置相同的类。

  • 如果装置在conftest.py文件中定义,那么在相同目录或以下目录中的测试

换句话说,如果一个测试可以通过在参数列表中声明它来访问一个autouse装置,那么该测试将自动使用autouse装置。请注意,如果测试函数对装置的返回值感兴趣,它可能会将autouse装置添加到其参数列表中,就像正常情况一样。

@pytest.mark.usefixtures

@pytest.mark.usefixtures标记可用于将一个或多个装置应用于测试,就好像它们在参数列表中声明了装置名称一样。在您希望所有组中的测试始终使用不是autouse的装置的情况下,这可能是一种替代方法。

例如,下面的代码将确保TestVirtualEnv类中的所有测试方法在一个全新的虚拟环境中执行:

@pytest.fixture
def venv_dir():
    import venv

    with tempfile.TemporaryDirectory() as d:
        venv.create(d)
        pwd = os.getcwd()
        os.chdir(d)
        yield d
        os.chdir(pwd)

@pytest.mark.usefixtures('venv_dir')
class TestVirtualEnv:
    ...

正如名称所示,您可以将多个装置名称传递给装饰器:

@pytest.mark.usefixtures("venv_dir", "config_python_debug")
class Test:
    ...

参数化装置

装置也可以直接进行参数化。当一个装置被参数化时,所有使用该装置的测试现在将多次运行,每个参数运行一次。当我们有装置的变体,并且每个使用该装置的测试也应该与所有变体一起运行时,这是一个很好的工具。

在上一章中,我们看到了使用序列化器的多个实现进行参数化的示例:

@pytest.mark.parametrize(
    "serializer_class",
    [JSONSerializer, XMLSerializer, YAMLSerializer],
)
class Test:

    def test_quantity(self, serializer_class):
        serializer = serializer_class()
        quantity = Quantity(10, "m")
        data = serializer.serialize_quantity(quantity)
        new_quantity = serializer.deserialize_quantity(data)
        assert new_quantity == quantity

    def test_pipe(self, serializer_class):
        serializer = serializer_class()
        pipe = Pipe(
            length=Quantity(1000, "m"), diameter=Quantity(35, "cm")
        )
       data = serializer.serialize_pipe(pipe)
       new_pipe = serializer.deserialize_pipe(data)
       assert new_pipe == pipe

我们可以更新示例以在装置上进行参数化:

class Test:

 @pytest.fixture(params=[JSONSerializer, XMLSerializer,
 YAMLSerializer])
 def serializer(self, request):
 return request.param()

    def test_quantity(self, serializer):
        quantity = Quantity(10, "m")
        data = serializer.serialize_quantity(quantity)
        new_quantity = serializer.deserialize_quantity(data)
        assert new_quantity == quantity

    def test_pipe(self, serializer):
        pipe = Pipe(
            length=Quantity(1000, "m"), diameter=Quantity(35, "cm")
        )
        data = serializer.serialize_pipe(pipe)
        new_pipe = serializer.deserialize_pipe(data)
        assert new_pipe == pipe

请注意以下内容:

  • 我们向装置定义传递了一个params参数。

  • 我们使用request对象的特殊param属性在装置内部访问参数。当装置被参数化时,这个内置装置提供了对请求测试函数和参数的访问。我们将在本章后面更多地了解request装置。

  • 在这种情况下,我们在装置内部实例化序列化器,而不是在每个测试中显式实例化。

可以看到,参数化装置与参数化测试非常相似,但有一个关键的区别:通过参数化装置,我们使所有使用该装置的测试针对所有参数化的实例运行,使它们成为conftest.py文件中共享的装置的绝佳解决方案。

当您向现有装置添加新参数时,看到自动执行了许多新测试是非常有益的。

使用装置标记

我们可以使用request装置来访问应用于测试函数的标记。

假设我们有一个autouse装置,它总是将当前区域初始化为英语:

@pytest.fixture(autouse=True)
def setup_locale():
    locale.setlocale(locale.LC_ALL, "en_US")
    yield
    locale.setlocale(locale.LC_ALL, None)

def test_currency_us():
    assert locale.currency(10.5) == "$10.50"

但是,如果我们只想为一些测试使用不同的区域设置呢?

一种方法是使用自定义标记,并在我们的装置内部访问mark对象:

@pytest.fixture(autouse=True)
def setup_locale(request):
    mark = request.node.get_closest_marker("change_locale")
    loc = mark.args[0] if mark is not None else "en_US"
    locale.setlocale(locale.LC_ALL, loc)
    yield
    locale.setlocale(locale.LC_ALL, None)

@pytest.mark.change_locale("pt_BR")
def test_currency_br():
    assert locale.currency(10.5) == "R$ 10,50"

标记可以用来将信息传递给装置。因为它有点隐式,所以我建议节俭使用,因为它可能导致难以理解的代码。

内置装置概述

让我们来看一些内置的 pytest 装置。

tmpdir

tmpdir装置提供了一个在每次测试结束时自动删除的空目录:

def test_empty(tmpdir):
    assert os.path.isdir(tmpdir)
    assert os.listdir(tmpdir) == []

作为function-scoped 装置,每个测试都有自己的目录,因此它们不必担心清理或生成唯一的目录。

装置提供了一个py.local对象(py.readthedocs.io/en/latest/path.html),来自py库(py.readthedocs.io),它提供了方便的方法来处理文件路径,比如连接,读取,写入,获取扩展名等等;它在哲学上类似于标准库中的pathlib.Path对象(docs.python.org/3/library/pathlib.html):

def test_save_curves(tmpdir):
    data = dict(status_code=200, values=[225, 300])
    fn = tmpdir.join('somefile.json')
    write_json(fn, data)
    assert fn.read() == '{"status_code": 200, "values": [225, 300]}'

为什么 pytest 使用py.local而不是pathlib.Path

pathlib.Path出现并被合并到标准库之前,Pytest 已经存在多年了,而py库是当时路径类对象的最佳解决方案之一。核心 pytest 开发人员正在研究如何使 pytest 适应现在标准的pathlib.PathAPI。

tmpdir_factory

tmpdir装置非常方便,但它只有function-scoped:这样做的缺点是它只能被其他function-scoped 装置使用。

tmpdir_factory装置是一个session-scoped装置,允许在任何范围内创建空的唯一目录。当我们需要在其他范围的装置中存储数据时,例如session-scoped 缓存或数据库文件时,这可能很有用。

为了展示它的作用,接下来显示的images_dir装置使用tmpdir_factory创建一个唯一的目录,整个测试会话中包含一系列示例图像文件:

@pytest.fixture(scope='session')
def images_dir(tmpdir_factory):
    directory = tmpdir_factory.mktemp('images')
    download_images('https://example.com/samples.zip', directory)
    extract_images(directory / 'samples.zip')
    return directory

因为这将每个会话只执行一次,所以在运行测试时会节省我们相当多的时间。

然后测试可以使用images_dir装置轻松访问示例图像文件:

def test_blur_filter(images_dir):
    output_image = apply_blur_filter(images_dir / 'rock1.png')
    ...

但请记住,此装置创建的目录是共享的,并且只会在测试会话结束时被删除。这意味着测试不应修改目录的内容;否则,它们可能会影响其他测试。

猴子补丁

在某些情况下,测试需要复杂或难以在测试环境中设置的功能,例如:

  • 对外部资源的客户端(例如 GitHub 的 API)需要在测试期间访问可能不切实际或成本太高

  • 强制代码表现得好像在另一个平台上,比如错误处理

  • 复杂的条件或难以在本地或 CI 中重现的环境

monkeypatch装置允许您使用其他对象和函数干净地覆盖正在测试的系统的函数、对象和字典条目,并在测试拆卸期间撤消所有更改。例如:

import getpass

def user_login(name):
    password = getpass.getpass()
    check_credentials(name, password)
    ...

在这段代码中,user_login使用标准库中的getpass.getpass()函数(docs.python.org/3/library/getpass.html)以系统中最安全的方式提示用户输入密码。在测试期间很难模拟实际输入密码,因为getpass尝试直接从终端读取(而不是从sys.stdin)。

我们可以使用monkeypatch装置来在测试中绕过对getpass的调用,透明地而不改变应用程序代码:

def test_login_success(monkeypatch):
    monkeypatch.setattr(getpass, "getpass", lambda: "valid-pass")
    assert user_login("test-user")

def test_login_wrong_password(monkeypatch):
    monkeypatch.setattr(getpass, "getpass", lambda: "wrong-pass")
    with pytest.raises(AuthenticationError, match="wrong password"):
        user_login("test-user")

在测试中,我们使用monkeypatch.setattr来用一个虚拟的lambda替换getpass模块的真实getpass()函数,它返回一个硬编码的密码。在test_login_success中,我们返回一个已知的好密码,以确保用户可以成功进行身份验证,而在test_login_wrong_password中,我们使用一个错误的密码来确保正确处理身份验证错误。如前所述,原始的getpass()函数会在测试结束时自动恢复,确保我们不会将该更改泄漏到系统中的其他测试中。

如何和在哪里修补

monkeypatch装置通过用另一个对象(通常称为模拟)替换对象的属性来工作,在测试结束时恢复原始对象。使用此装置的常见问题是修补错误的对象,这会导致调用原始函数/对象而不是模拟函数/对象。

要理解问题,我们需要了解 Python 中importimport from的工作原理。

考虑一个名为services.py的模块:

import subprocess

def start_service(service_name):
    subprocess.run(f"docker run {service_name}")

在这段代码中,我们导入subprocess模块并将subprocess模块对象引入services.py命名空间。这就是为什么我们调用subprocess.run:我们正在访问services.py命名空间中subprocess对象的run函数。

现在考虑稍微不同的以前的代码写法:

from subprocess import run

def start_service(service_name):
    run(f"docker run {service_name}")

在这里,我们导入了subprocess模块,但将run函数对象带入了service.py命名空间。这就是为什么run可以直接在start_service中调用,而subprocess名称甚至不可用(如果尝试调用subprocess.run,将会得到NameError异常)。

我们需要意识到这种差异,以便正确地monkeypatchservices.py中使用subprocess.run

在第一种情况下,我们需要替换subprocess模块的run函数,因为start_service就是这样使用它的:

import subprocess
import services

def test_start_service(monkeypatch):
    commands = []
    monkeypatch.setattr(subprocess, "run", commands.append)
    services.start_service("web")
    assert commands == ["docker run web"]

在这段代码中,services.pytest_services.py都引用了相同的subprocess模块对象。

然而,在第二种情况下,services.py在自己的命名空间中引用了原始的run函数。因此,第二种情况的正确方法是替换services.py命名空间中的run函数:

import services

def test_start_service(monkeypatch):
    commands = []
    monkeypatch.setattr(services, "run", commands.append)
    services.start_service("web")
    assert commands == ["docker run web"]

被测试代码导入需要进行 monkeypatch 的代码是人们经常被绊倒的原因,所以确保您首先查看代码。

capsys/capfd

capsys fixture 捕获了写入sys.stdoutsys.stderr的所有文本,并在测试期间使其可用。

假设我们有一个小的命令行脚本,并且希望在调用脚本时没有参数时检查使用说明是否正确:

from textwrap import dedent

def script_main(args):
    if not args:
        show_usage()
        return 0
    ...

def show_usage():
    print("Create/update webhooks.")
    print(" Usage: hooks REPO URL")

在测试期间,我们可以使用capsys fixture 访问捕获的输出。这个 fixture 有一个capsys.readouterr()方法,返回一个namedtuple(docs.python.org/3/library/collections.html#collections.namedtuple),其中包含从sys.stdoutsys.stderr捕获的文本。

def test_usage(capsys):
    script_main([])
    captured = capsys.readouterr()
    assert captured.out == dedent("""\
        Create/update webhooks.
          Usage: hooks REPO URL
    """)

还有capfd fixture,它的工作方式类似于capsys,只是它还捕获文件描述符12的输出。这使得可以捕获标准输出和标准错误,即使是对于扩展模块。

二进制模式

capsysbinarycapfdbinary是与capsyscapfd相同的 fixtures,不同之处在于它们以二进制模式捕获输出,并且它们的readouterr()方法返回原始字节而不是文本。在特殊情况下可能会有用,例如运行生成二进制输出的外部进程时,如tar

request

request fixture 是一个内部 pytest fixture,提供有关请求测试的有用信息。它可以在测试函数和 fixtures 中声明,并提供以下属性:

  • function:Python test函数对象,可用于function-scoped fixtures。

  • cls/instance:Python 类/实例的test方法对象,可用于functionclass-scoped fixtures。如果 fixture 是从test函数请求的,而不是测试方法,则可以为None

  • module:请求测试方法的 Python 模块对象,可用于modulefunctionclass-scoped fixtures。

  • session:pytest 的内部Session对象,它是测试会话的单例,代表集合树的根。它可用于所有范围的 fixtures。

  • node:pytest 集合节点,它包装了与 fixture 范围匹配的 Python 对象之一。

  • addfinalizer(func): 添加一个将在测试结束时调用的new finalizer函数。finalizer 函数将在不带参数的情况下调用。addfinalizer是在 fixtures 中执行拆卸的原始方法,但后来已被yield语句取代,主要用于向后兼容。

fixtures 可以使用这些属性根据正在执行的测试自定义自己的行为。例如,我们可以创建一个 fixture,使用当前测试名称作为临时目录的前缀,类似于内置的tmpdir fixture:

@pytest.fixture
def tmp_path(request) -> Path:
    with TemporaryDirectory(prefix=request.node.name) as d:
        yield Path(d)

def test_tmp_path(tmp_path):
    assert list(tmp_path.iterdir()) == []

在我的系统上执行此代码时创建了以下目录:

C:\Users\Bruno\AppData\Local\Temp\test_tmp_patht5w0cvd0

request fixture 可以在您想要根据正在执行的测试的属性自定义 fixture,或者访问应用于测试函数的标记时使用,正如我们在前面的部分中所看到的。

提示/讨论

以下是一些未适应前面部分的短话题和提示,但我认为值得一提。

何时使用 fixture,而不是简单函数

有时,您只需要为测试构造一个简单的对象,可以说这可以通过一个普通函数来完成,不一定需要实现为 fixture。假设我们有一个不接收任何参数的 WindowManager 类:

class WindowManager:
    ...

在我们的测试中使用它的一种方法是编写一个 fixture:

@pytest.fixture
def manager():
 return WindowManager()

def test_windows_creation(manager):
    window = manager.new_help_window("pipes_help.rst")
    assert window.title() == "Pipe Setup Help"

或者,您可以主张为这样简单的用法编写一个 fixture 是过度的,并且使用一个普通函数代替:

def create_window_manager():
    return WindowManager()

def test_windows_creation():
    manager = create_window_manager()
    window = manager.new_help_window("pipes_help.rst")
    assert window.title() == "Pipe Setup Help"

或者您甚至可以在每个测试中显式创建管理器:

def test_windows_creation():
    manager = WindowManager()
    window = manager.new_help_window("pipes_help.rst")
    assert window.title() == "Pipe Setup Help"

这是完全可以的,特别是如果在单个模块中的少数测试中使用。

然而,请记住,fixture 抽象了对象的构建和拆卸过程的细节。在决定放弃 fixture 而选择普通函数时,这一点至关重要。

假设我们的 WindowManager 现在需要显式关闭,或者它需要一个本地目录用于记录目的:

class WindowManager:

    def __init__(self, logging_directory):
        ...

    def close(self):
        """
        Close the WindowManager and all associated resources. 
        """
        ...

如果我们一直在使用像第一个例子中给出的 fixture,我们只需更新 fixture 函数,测试根本不需要改变

@pytest.fixture
def manager(tmpdir):
    wm = WindowManager(str(tmpdir))
    yield wm
 wm.close()

但是,如果我们选择使用一个普通函数,现在我们必须更新调用我们函数的所有地方:我们需要传递一个记录目录,并确保在测试结束时调用 .close()

def create_window_manager(tmpdir, request):
    wm = WindowManager(str(tmpdir))
    request.addfinalizer(wm.close)
    return wm

def test_windows_creation(tmpdir, request):
    manager = create_window_manager(tmpdir, request)
    window = manager.new_help_window("pipes_help.rst")
    assert window.title() == "Pipe Setup Help"

根据这个函数在我们的测试中被使用的次数,这可能是一个相当大的重构。

这个信息是:当底层对象简单且不太可能改变时,使用普通函数是可以的,但请记住,fixture 抽象了对象的创建/销毁的细节,它们可能在将来需要更改。另一方面,使用 fixture 创建了另一个间接层,稍微增加了代码复杂性。最终,这是一个需要您权衡的平衡。

重命名 fixture

@pytest.fixture 装饰器接受一个 name 参数,该参数可用于指定 fixture 的名称,与 fixture 函数不同:

@pytest.fixture(name="venv_dir")
def _venv_dir():
    ...

这是有用的,因为有一些烦恼可能会影响用户在使用在相同模块中声明的 fixture 时:

  • 如果用户忘记在测试函数的参数列表中声明 fixture,他们将得到一个 NameError,而不是 fixture 函数对象(因为它们在同一个模块中)。

  • 一些 linters 抱怨测试函数参数遮蔽了 fixture 函数。

如果之前的烦恼经常发生,您可能会将这视为团队中的一个良好实践。请记住,这些问题只会发生在测试模块中定义的 fixture 中,而不会发生在 conftest.py 文件中。

在 conftest 文件中优先使用本地导入

conftest.py 文件在收集期间被导入,因此它们直接影响您从命令行运行测试时的体验。因此,我建议在 conftest.py 文件中尽可能使用本地导入,以保持导入时间较短。

因此,不要使用这个:

import pytest
import tempfile
from myapp import setup

@pytest.fixture
def setup_app():
    ...

优先使用本地导入:

import pytest

@pytest.fixture
def setup_app():
 import tempfile
 from myapp import setup
    ...

这种做法对大型测试套件的启动有明显影响。

fixture 作为测试支持代码

您应该将 fixture 视为不仅提供资源的手段,还提供测试的支持代码。通过支持代码,我指的是为测试提供高级功能的类。

例如,一个机器人框架可能会提供一个 fixture,用于测试您的机器人作为黑盒:

def test_hello(bot):
    reply = bot.say("hello")
    assert reply.text == "Hey, how can I help you?"

def test_store_deploy_token(bot):
    assert bot.store["TEST"]["token"] is None
    reply = bot.say("my token is ASLKM8KJAN")
    assert reply.text == "OK, your token was saved"
    assert bot.store["TEST"]["token"] == "ASLKM8KJAN"

bot fixture 允许开发人员与机器人交谈,验证响应,并检查框架处理的内部存储的内容,等等。它提供了一个高级接口,使得测试更容易编写和理解,即使对于那些不了解框架内部的人也是如此。

这种技术对应用程序很有用,因为它将使开发人员轻松愉快地添加新的测试。对于库来说也很有用,因为它们将为库的用户提供高级测试支持。

总结

在本章中,我们深入了解了 pytest 最著名的功能之一:fixtures。我们看到了它们如何被用来提供资源和测试功能,以及如何简洁地表达设置/拆卸代码。我们学会了如何共享 fixtures,使用conftest.py文件;如何使用 fixture scopes,避免为每个测试创建昂贵的资源;以及如何自动使用 fixtures,这些 fixtures 会在同一模块或层次结构中的所有测试中执行。然后,我们学会了如何对 fixtures 进行参数化,并从中使用标记。我们对各种内置 fixtures 进行了概述,并在最后对 fixtures 进行了一些简短的讨论。希望您喜欢这一过程!

在下一章中,我们将探索一下广阔的 pytest 插件生态系统,这些插件都可以供您使用。

第四章:插件

在前一章中,我们探讨了 pytest 最重要的特性之一:fixture。我们学会了如何使用 fixture 来管理资源,并在编写测试时让我们的生活更轻松。

pytest 是以定制和灵活性为目标构建的,并允许开发人员编写称为插件的强大扩展。pytest 中的插件可以做各种事情,从简单地提供新的 fixture,到添加命令行选项,改变测试的执行方式,甚至运行用其他语言编写的测试。

在本章中,我们将做以下事情:

  • 学习如何查找和安装插件

  • 品尝生态系统提供的插件

查找和安装插件

正如本章开头提到的,pytest 是从头开始以定制和灵活性为目标编写的。插件机制是 pytest 架构的核心,以至于 pytest 的许多内置功能都是以内部插件的形式实现的,比如标记、参数化、fixture——几乎所有东西,甚至命令行选项。

这种灵活性导致了一个庞大而丰富的插件生态系统。在撰写本文时,可用的插件数量已经超过 500 个,而且这个数字以惊人的速度不断增加。

查找插件

考虑到插件的数量众多,如果有一个网站能够展示所有 pytest 插件以及它们的描述,那将是很好的。如果这个地方还能显示关于不同 Python 和 pytest 版本的兼容性信息,那就更好了。

好消息是,这样的网站已经存在了,并且由核心开发团队维护:pytest 插件兼容性(plugincompat.herokuapp.com/)。在这个网站上,你将找到 PyPI 中所有可用的 pytest 插件的列表,以及 Python 和 pytest 版本的兼容性信息。该网站每天都会从 PyPI 直接获取新的插件和更新,是一个浏览新插件的好地方。

安装插件

插件通常使用pip安装:

λ pip install <PLUGIN_NAME>

例如,要安装pytest-mock,我们执行以下操作:

λ pip install pytest-mock

不需要任何注册;pytest 会自动检测你的虚拟环境或 Python 安装中安装的插件。

这种简单性使得尝试新插件变得非常容易。

各种插件概述

现在,我们将看一些有用和/或有趣的插件。当然,不可能在这里覆盖所有的插件,所以我们将尝试覆盖那些涵盖流行框架和一般功能的插件,还有一些晦涩的插件。当然,这只是皮毛,但让我们开始吧。

pytest-xdist

这是一个非常受欢迎的插件,由核心开发人员维护;它允许你在多个 CPU 下运行测试,以加快测试运行速度。

安装后,只需使用-n命令行标志来使用给定数量的 CPU 来运行测试:

λ pytest -n 4

就是这样!现在,你的测试将在四个核心上运行,希望能够加快测试套件的速度,如果测试是 CPU 密集型的话,尽管 I/O 绑定的测试不会看到太多改进。你也可以使用-n auto来让pytest-xdist自动计算出你可用的 CPU 数量。

请记住,当你的测试并行运行,并且以随机顺序运行时,它们必须小心避免相互干扰,例如,读/写到同一个目录。虽然它们应该是幂等的,但以随机顺序运行测试通常会引起之前潜伏的问题。

pytest-cov

pytest-cov插件与流行的 coverage 模块集成,当运行测试时提供详细的覆盖报告。这让你可以检测到没有被任何测试代码覆盖的代码部分,这是一个机会,可以编写更多的测试来覆盖这些情况。

安装后,您可以使用--cov选项在测试运行结束时提供覆盖报告:

λ pytest --cov=src
...
----------- coverage: platform win32, python 3.6.3-final-0 -----------
Name                  Stmts   Miss  Cover
----------------------------------------
src/series.py           108      5   96%
src/tests/test_series    22      0  100%
----------------------------------------
TOTAL                   130      5   97%

--cov选项接受应生成报告的源文件路径,因此根据项目的布局,您应传递您的src或包目录。

您还可以使用--cov-report选项以生成各种格式的报告:XML,annotate 和 HTML。后者特别适用于本地使用,因为它生成 HTML 文件,显示您的代码,未覆盖的行以红色突出显示,非常容易找到这些未覆盖的地方。

此插件还可以与pytest-xdist直接使用。

最后,此插件生成的.coverage文件与许多提供覆盖跟踪和报告的在线服务兼容,例如coveralls.iocoveralls.io/)和codecov.iocodecov.io/)。

pytest-faulthandler

此插件在运行测试时自动启用内置的faulthandlerdocs.python.org/3/library/faulthandler.html)模块,该模块在灾难性情况下(如分段错误)输出 Python 回溯。安装后,无需其他设置或标志;faulthandler模块将自动启用。

如果您经常使用用 C/C++编写的扩展模块,则强烈建议使用此插件,因为这些模块更容易崩溃。

pytest-mock

pytest-mock插件提供了一个 fixture,允许 pytest 和标准库的unittest.mockdocs.python.org/3/library/unittest.mock.html)模块之间更顺畅地集成。它提供了类似于内置的monkeypatch fixture 的功能,但是unittest.mock产生的模拟对象还记录有关它们如何被访问的信息。这使得许多常见的测试任务更容易,例如验证已调用模拟函数以及使用哪些参数。

该插件提供了一个mocker fixture,可用于修补类和方法。使用上一章中的getpass示例,以下是您可以使用此插件编写它的方式:

import getpass

def test_login_success(mocker):
    mocked = mocker.patch.object(getpass, "getpass", 
                                 return_value="valid-pass")
    assert user_login("test-user")
    mocked.assert_called_with("enter password: ")

请注意,除了替换getpass.getpass()并始终返回相同的值之外,我们还可以确保getpass函数已使用正确的参数调用。

在使用此插件时,与上一章中如何以及在哪里修补monkeypatch fixture 的建议也适用。

pytest-django

顾名思义,此插件允许您使用 pytest 测试您的Djangowww.djangoproject.com/)应用程序。Django是当今最著名的 Web 框架之一。

该插件提供了大量功能:

  • 一个非常好的快速入门教程

  • 命令行和pytest.ini选项来配置 Django

  • pytest-xdist兼容

  • 使用django_db标记访问数据库,在测试之间自动回滚事务,以及一堆 fixture,让您控制数据库的管理方式

  • 用于向应用程序发出请求的 fixture:clientadmin_clientadmin_user

  • 在后台线程中运行Django服务器的live_server fixture

总的来说,这是生态系统中最完整的插件之一,具有太多功能无法在此处覆盖。对于Django应用程序来说,这是必不可少的,因此请务必查看其广泛的文档。

pytest-flakes

此插件允许您使用pyflakespypi.org/project/pyflakes/)检查您的代码,这是一个用于常见错误的源文件的静态检查器,例如丢失的导入和未知变量。

安装后,使用--flakes选项来激活它:

λ pytest pytest-flakes.py --flake
...
============================= FAILURES ==============================
__________________________ pyflakes-check ___________________________
CH5\pytest-flakes.py:1: UnusedImport
'os' imported but unused
CH5\pytest-flakes.py:6: UndefinedName
undefined name 'unknown'

这将在你的正常测试中运行 flake 检查,使其成为保持代码整洁和防止一些错误的简单而廉价的方法。该插件还保留了自上次检查以来未更改的文件的本地缓存,因此在本地使用起来快速和方便。

pytest-asyncio

asyncio (docs.python.org/3/library/asyncio.html)模块是 Python 3 的热门新功能之一,提供了一个新的用于异步应用程序的框架。pytest-asyncio插件让你编写异步测试函数,轻松测试你的异步代码。

你只需要将你的测试函数标记为async def并使用asyncio标记:

@pytest.mark.asyncio
async def test_fetch_requests():
    requests = await fetch_requests("example.com/api")
    assert len(requests) == 2

该插件还在后台管理事件循环,提供了一些选项,以便在需要使用自定义事件循环时进行更改。

当然,你可以在异步函数之外拥有正常的同步测试函数。

pytest-trio

Trio 的座右铭是“Pythonic async I/O for humans” (trio.readthedocs.io/en/latest/)。它使用与asyncio标准模块相同的async def/await关键字,但被认为更简单和更友好,包含一些关于如何处理超时和一组并行任务的新颖想法,以避免并行编程中的常见错误。如果你对异步开发感兴趣,它绝对值得一试。

pytest-trio的工作方式类似于pytest-asyncio:你编写异步测试函数,并使用trio标记它们。它还提供了其他功能,使测试更容易和更可靠,例如可控的时钟用于测试超时,处理任务的特殊函数,模拟网络套接字和流,以及更多。

pytest-tornado

Tornado (www.tornadoweb.org/en/stable/)是一个 Web 框架和异步网络库。它非常成熟,在 Python 2 和 3 中工作,标准的asyncio模块从中借鉴了许多想法和概念。

pytest-asynciopytest-tornado的启发,因此它使用相同的想法,使用gen_test来标记你的测试为协程。它使用yield关键字而不是await,因为它支持 Python 2,但除此之外它看起来非常相似:

@pytest.mark.gen_test
def test_tornado(http_client):
    url = "https://docs.pytest.org/en/latest"
    response = yield http_client.fetch(url)
    assert response.code == 200

pytest-postgresql

该插件允许你测试需要运行的 PostgreSQL 数据库的代码。

以下是它的一个快速示例:

def test_fetch_series(postgresql):
    cur = postgresql.cursor()
    cur.execute('SELECT * FROM comedy_series;')
    assert len(cur.fetchall()) == 5
    cur.close()

它提供了两个 fixtures:

  • postgresql:一个客户端 fixture,启动并关闭到正在运行的测试数据库的连接。在测试结束时,它会删除测试数据库,以确保测试不会相互干扰。

  • postgresql_proc:一个会话范围的 fixture,每个会话启动一次 PostgreSQL 进程,并确保在结束时停止。

它还提供了几个配置选项,用于连接和配置测试数据库。

docker-services

该插件启动和管理你需要的 Docker 服务,以便测试你的代码。这使得运行测试变得简单,因为你不需要手动启动服务;插件将在测试会话期间根据需要启动和停止它们。

你可以使用.services.yaml文件来配置服务;这里是一个简单的例子:

database:
    image: postgres
    environment:
        POSTGRES_USERNAME: pytest-user
        POSTGRES_PASSWORD: pytest-pass
        POSTGRES_DB: test
    image: regis:10 

这将启动两个服务:postgresredis

有了这个,剩下的就是用以下命令运行你的套件:

pytest --docker-services

插件会处理剩下的事情。

pytest-selenium

Selenium 是一个针对自动化浏览器的框架,用于测试 Web 应用程序 (www.seleniumhq.org/)。它可以做诸如打开网页、点击按钮,然后确保某个页面加载等事情。它支持所有主流浏览器,并拥有一个蓬勃发展的社区。

pytest-selenium提供了一个 fixture,让你编写测试来完成所有这些事情,它会为你设置Selenium

以下是如何访问页面,点击链接并检查加载页面的标题的基本示例:

def test_visit_pytest(selenium):
    selenium.get("https://docs.pytest.org/en/latest/")
    assert "helps you write better programs" in selenium.title
    elem = selenium.find_element_by_link_text("Contents")
    elem.click()
    assert "Full pytest documentation" in selenium.title

Seleniumpytest-selenium足够复杂,可以测试从静态页面到完整的单页前端应用程序的各种应用。

pytest-html

pytest-html 生成美丽的 HTML 测试结果报告。安装插件后,只需运行以下命令:

λ pytest --html=report.html

这将在测试会话结束时生成一个report.html文件。

因为图片胜过千言万语,这里有一个例子:

报告可以在 Web 服务器上进行服务以便更轻松地查看,而且它们包含了一些很好的功能,比如复选框来显示/隐藏不同类型的测试结果,还有其他插件如pytest-selenium甚至能够在失败的测试中附加截图,就像前面的图片一样。

它绝对值得一试。

pytest-cpp

为了证明 pytest 框架非常灵活,pytest-cpp插件允许你运行用 Google Test (github.com/google/googletest) 或 Boost.Test (www.boost.org)编写的测试,这些是用 C++语言编写和运行测试的框架。

安装后,你只需要像平常一样运行 pytest:

λ pytest bin/tests

Pytest 将找到包含测试用例的可执行文件,并自动检测它们是用Google Test还是Boost.Python编写的。它将正常运行测试并报告结果,格式整齐,熟悉 pytest 用户。

使用 pytest 运行这些测试意味着它们现在可以利用一些功能,比如使用pytest-xdist进行并行运行,使用-k进行测试选择,生成 JUnitXML 报告等等。这个插件对于使用 Python 和 C++的代码库特别有用,因为它允许你用一个命令运行所有测试,并且你可以得到一个独特的报告。

pytest-timeout

pytest-timeout插件在测试达到一定超时后会自动终止测试。

你可以通过在命令行中设置全局超时来使用它:

λ pytest --timeout=60

或者你可以使用@pytest.mark.timeout标记单独的测试:

@pytest.mark.timeout(600)
def test_long_simulation():
   ...

它通过以下两种方法之一来实现超时机制:

  • thread:在测试设置期间,插件启动一个线程,该线程休眠指定的超时时间。如果线程醒来,它将将所有线程的回溯信息转储到stderr并杀死当前进程。如果测试在线程醒来之前完成,那么线程将被取消,测试继续运行。这是在所有平台上都有效的方法。

  • signal:在测试设置期间安排了一个SIGALRM,并在测试完成时取消。如果警报被触发,它将将所有线程的回溯信息转储到stderr并失败测试,但它将允许测试继续运行。与线程方法相比的优势是当超时发生时它不会取消整个运行,但它不支持所有平台。

该方法会根据平台自动选择,但可以在命令行或通过@pytest.mark.timeoutmethod=参数来进行更改。

这个插件在大型测试套件中是不可或缺的,以避免测试挂起 CI。

pytest-annotate

Pyannotate (github.com/dropbox/pyannotate) 是一个观察运行时类型信息并将该信息插入到源代码中的项目,而pytest-annotate使得在 pytest 中使用它变得很容易。

让我们回到这个简单的测试用例:

def highest_rated(series):
    return sorted(series, key=itemgetter(2))[-1][0]

def test_highest_rated():
    series = [
        ("The Office", 2005, 8.8),
        ("Scrubs", 2001, 8.4),
        ("IT Crowd", 2006, 8.5),
        ("Parks and Recreation", 2009, 8.6),
        ("Seinfeld", 1989, 8.9),
    ]
    assert highest_rated(series) == "Seinfeld"

安装了pytest-annotate后,我们可以通过传递--annotations-output标志来生成一个注释文件:

λ pytest --annotate-output=annotations.json

这将像往常一样运行测试套件,但它将收集类型信息以供以后使用。

之后,你可以调用PyAnnotate将类型信息直接应用到源代码中:

λ pyannotate --type-info annotations.json -w
Refactored test_series.py
--- test_series.py (original)
+++ test_series.py (refactored)
@@ -1,11 +1,15 @@
 from operator import itemgetter
+from typing import List
+from typing import Tuple

 def highest_rated(series):
+    # type: (List[Tuple[str, int, float]]) -> str
 return sorted(series, key=itemgetter(2))[-1][0]

 def test_highest_rated():
+    # type: () -> None
 series = [
 ("The Office", 2005, 8.8),
 ("Scrubs", 2001, 8.4),
Files that were modified:
pytest-annotate.py

快速高效地注释大型代码库是非常整洁的,特别是如果该代码库已经有了完善的测试覆盖。

pytest-qt

pytest-qt插件允许您为使用Qt框架(www.qt.io/)编写的 GUI 应用程序编写测试,支持更受欢迎的 Python 绑定集:PyQt4/PyQt5PySide/PySide2

它提供了一个qtbot装置,其中包含与 GUI 应用程序交互的方法,例如单击按钮、在字段中输入文本、等待窗口弹出等。以下是一个快速示例,展示了它的工作原理:

def test_main_window(qtbot):
    widget = MainWindow()
    qtbot.addWidget(widget)

    qtbot.mouseClick(widget.about_button, QtCore.Qt.LeftButton)
    qtbot.waitUntil(widget.about_box.isVisible)
    assert widget.about_box.text() == 'This is a GUI App'

在这里,我们创建一个窗口,单击“关于”按钮,等待“关于”框弹出,然后确保它显示我们期望的文本。

它还包含其他好东西:

  • 等待特定Qt信号的实用程序

  • 自动捕获虚拟方法中的错误

  • 自动捕获Qt日志消息

pytest-randomly

测试理想情况下应该是相互独立的,确保在测试完成后进行清理,这样它们可以以任何顺序运行,而且不会以任何方式相互影响。

pytest-randomly通过随机排序测试,每次运行测试套件时更改它们的顺序,帮助您保持测试套件的真实性。这有助于检测测试是否具有隐藏的相互依赖性,否则您将无法发现。

它会在模块级别、类级别和函数顺序上对测试项进行洗牌。它还会在每个测试之前将random.seed()重置为一个固定的数字,该数字显示在测试部分的开头。可以在以后使用随机种子通过--randomly-seed命令行来重现失败。

作为额外的奖励,它还特别支持factory boyfactoryboy.readthedocs.io/en/latest/reference.html)、fakerpypi.python.org/pypi/faker)和numpywww.numpy.org/)库,在每个测试之前重置它们的随机状态。

pytest-datadir

通常,测试需要一个支持文件,例如一个包含有关喜剧系列数据的 CSV 文件,就像我们在上一章中看到的那样。pytest-datadir允许您将文件保存在测试旁边,并以安全的方式从测试中轻松访问它们。

假设您有这样的文件结构:

tests/
    test_series.py

除此之外,您还有一个series.csv文件,需要从test_series.py中定义的测试中访问。

安装了pytest-datadir后,您只需要在相同目录中创建一个与测试文件同名的目录,并将文件放在其中:

tests/
 test_series/
 series.csv
    test_series.py

test_series目录和series.csv应该保存到您的版本控制系统中。

现在,test_series.py中的测试可以使用datadir装置来访问文件:

def test_ratings(datadir):
    with open(datadir / "series.csv", "r", newline="") as f:
        data = list(csv.reader(f))
    ...

datadir是一个指向数据目录的 Path 实例(docs.python.org/3/library/pathlib.html)。

需要注意的一点是,当我们在测试中使用datadir装置时,我们并不是访问原始文件的路径,而是临时副本。这确保了测试可以修改数据目录中的文件,而不会影响其他测试,因为每个测试都有自己的副本。

pytest-regressions

通常情况下,您的应用程序或库包含产生数据集作为结果的功能。

经常测试这些结果是很繁琐且容易出错的,产生了这样的测试:

def test_obtain_series_asserts():
    data = obtain_series()
    assert data[0]["name"] == "The Office"
    assert data[0]["year"] == 2005
    assert data[0]["rating"] == 8.8
    assert data[1]["name"] == "Scrubs"
    assert data[1]["year"] == 2001
    ...

这很快就会变得老套。此外,如果任何断言失败,那么测试就会在那一点停止,您将不知道在那一点之后是否还有其他断言失败。换句话说,您无法清楚地了解整体失败的情况。最重要的是,这也是非常难以维护的,因为如果obtain_series()返回的数据发生变化,您将不得不进行繁琐且容易出错的代码更新任务。

pytest-regressions提供了解决这类问题的装置。像前面的例子一样,一般的数据是data_regression装置的工作:

def test_obtain_series(data_regression):
    data = obtain_series()
    data_regression.check(data)

第一次执行此测试时,它将失败,并显示如下消息:

...
E Failed: File not found in data directory, created:
E - CH5\test_series\test_obtain_series.yml

它将以一个格式良好的 YAML 文件的形式将传递给data_regression.check()的数据转储到test_series.py文件的数据目录中(这要归功于我们之前看到的pytest-datadir装置):

- name: The Office
  rating: 8.8
  year: 2005
- name: Scrubs
  rating: 8.4
  year: 2001
- name: IT Crowd
  rating: 8.5
  year: 2006
- name: Parks and Recreation
  rating: 8.6
  year: 2009
- name: Seinfeld
  rating: 8.9
  year: 1989

下次运行此测试时,data_regression现在将传递给data_regressions.check()的数据与数据目录中的test_obtain_series.yml中找到的数据进行比较。如果它们匹配,测试通过。

然而,如果数据发生了变化,测试将失败,并显示新数据与记录数据之间的差异:

E AssertionError: FILES DIFFER:
E ---
E
E +++
E
E @@ -13,3 +13,6 @@
E
E  - name: Seinfeld
E    rating: 8.9
E    year: 1989
E +- name: Rock and Morty
E +  rating: 9.3
E +  year: 2013

在某些情况下,这可能是一个回归,这种情况下你可以在代码中找到错误。

但在这种情况下,新数据是正确的;你只需要用--force-regen标志运行 pytest,pytest-regressions将为你更新数据文件的新内容:

E Failed: Files differ and --force-regen set, regenerating file at:
E - CH5\test_series\test_obtain_series.yml

现在,如果我们再次运行测试,测试将通过,因为文件包含了新数据。

当你有数十个测试突然产生不同但正确的结果时,这将极大地节省时间。你可以通过单次 pytest 执行将它们全部更新。

我自己使用这个插件,我数不清它为我节省了多少时间。

值得一提的是

有太多好的插件无法放入本章。前面的示例只是一个小小的尝试,我试图在有用、有趣和展示插件架构的灵活性之间取得平衡。

以下是一些值得一提的其他插件:

  • pytest-bdd:pytest 的行为驱动开发

  • pytest-benchmark:用于对代码进行基准测试的装置。它以彩色输出输出基准测试结果

  • pytest-csv:将测试状态输出为 CSV 文件

  • pytest-docker-compose:在测试运行期间使用 Docker compose 管理 Docker 容器

  • pytest-excel:以 Excel 格式输出测试状态报告

  • pytest-git:为需要处理 git 仓库的测试提供 git 装置

  • pytest-json:将测试状态输出为 json 文件

  • pytest-leaks:通过重复运行测试并比较引用计数来检测内存泄漏

  • pytest-menu:允许用户从控制台菜单中选择要运行的测试

  • pytest-mongo:MongoDB 的进程和客户端装置

  • pytest-mpl:测试 Matplotlib 输出的图形的插件

  • pytest-mysql:MySQL 的进程和客户端装置

  • pytest-poo:用"pile of poo"表情符号替换失败测试的F字符

  • pytest-rabbitmq:RabbitMQ 的进程和客户端装置

  • pytest-redis:Redis 的进程和客户端装置

  • pytest-repeat:重复所有测试或特定测试多次以查找间歇性故障

  • pytest-replay:保存测试运行并允许用户以后执行它们,以便重现崩溃和不稳定的测试

  • pytest-rerunfailures:标记可以运行多次以消除不稳定测试的测试

  • pytest-sugar:通过添加进度条、表情符号、即时失败等来改变 pytest 控制台的外观和感觉

  • pytest-tap:以 TAP 格式输出测试报告

  • pytest-travis-fold:在 Travis CI 构建日志中折叠捕获的输出和覆盖报告

  • pytest-vagrant:与 vagrant boxes 一起使用的 pytest 装置

  • pytest-vcr:使用简单的标记自动管理VCR.py磁带

  • pytest-virtualenv:提供一个虚拟环境装置来管理测试中的虚拟环境

  • pytest-watch:持续监视源代码的更改并重新运行 pytest

  • pytest-xvfb:为 UI 测试运行Xvfb(虚拟帧缓冲区)

  • tavern:使用基于 YAML 的语法对 API 进行自动化测试

  • xdoctest:重写内置的 doctests 模块,使得编写和配置 doctests 更加容易

请记住,在撰写本文时,pytest 插件的数量已经超过 500 个,所以一定要浏览插件列表,以便找到自己喜欢的东西。

总结

在本章中,我们看到了查找和安装插件是多么容易。我们还展示了一些我每天使用并且觉得有趣的插件。我希望这让你对 pytest 的可能性有所了解,但请探索大量的插件,看看是否有任何有用的。

创建自己的插件不是本书涵盖的主题,但如果你感兴趣,这里有一些资源可以帮助你入门:

在下一章中,我们将学习如何将 pytest 与现有的基于unittest的测试套件一起使用,包括有关如何迁移它们并逐步使用更多 pytest 功能的提示和建议。

第五章:将 unittest 套件转换为 pytest

在上一章中,我们已经看到了灵活的 pytest 架构如何创建了丰富的插件生态系统,拥有数百个可用的插件。我们学习了如何轻松找到和安装插件,并概述了一些有趣的插件。

现在您已经熟练掌握 pytest,您可能会遇到这样的情况,即您有一个或多个基于unittest的测试套件,并且希望开始使用 pytest 进行测试。在本章中,我们将讨论从简单的测试套件开始做到这一点的最佳方法,这可能需要很少或根本不需要修改,到包含多年来有机地增长的各种自定义的大型内部测试套件。本章中的大多数提示和建议都来自于我在 ESSS(wwww.esss.co)工作时迁移我们庞大的unittest风格测试套件的经验。

以下是本章将涵盖的内容:

  • 使用 pytest 作为测试运行器

  • 使用unittest2pytest转换断言

  • 处理设置和拆卸

  • 管理测试层次结构

  • 重构测试工具

  • 迁移策略

使用 pytest 作为测试运行器

令人惊讶的是,许多人不知道的一件事是,pytest 可以直接运行unittest套件,无需任何修改。

例如:

class Test(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.temp_dir = Path(tempfile.mkdtemp())
        cls.filepath = cls.temp_dir / "data.csv"
        cls.filepath.write_text(DATA.strip())

    @classmethod
    def tearDownClass(cls):
        shutil.rmtree(cls.temp_dir)

    def setUp(self):
        self.grids = list(iter_grids_from_csv(self.filepath))

    def test_read_properties(self):
        self.assertEqual(self.grids[0], GridData("Main Grid", 48, 44))
        self.assertEqual(self.grids[1], GridData("2nd Grid", 24, 21))
        self.assertEqual(self.grids[2], GridData("3rd Grid", 24, 48))

    def test_invalid_path(self):
        with self.assertRaises(IOError):
            list(iter_grids_from_csv(Path("invalid file")))

    @unittest.expectedFailure
    def test_write_properties(self):
        self.fail("not implemented yet")

我们可以使用unittest运行器来运行这个:

..x
----------------------------------------------------------------------
Ran 3 tests in 0.005s

OK (expected failures=1)

但很酷的是,pytest 也可以在不进行任何修改的情况下运行此测试:

λ pytest test_simple.py
======================== test session starts ========================
...
collected 3 items

test_simple.py ..x                                             [100%]

================ 2 passed, 1 xfailed in 0.11 seconds ================

这使得使用 pytest 作为测试运行器变得非常容易,带来了几个好处:

  • 您可以使用插件,例如pytest-xdist,来加速测试套件。

  • 您可以使用几个命令行选项:-k选择测试,--pdb在错误时跳转到调试器,--lf仅运行上次失败的测试,等等。

  • 您可以停止编写self.assert*方法,改用普通的assert。 pytest 将愉快地提供丰富的失败信息,即使对于基于unittest的子类也是如此。

为了完整起见,以下是直接支持的unittest习语和功能:

  • setUptearDown用于函数级setup/teardown

  • setUpClasstearDownClass用于类级setup/teardown

  • setUpModuletearDownModule用于模块级setup/teardown

  • skipskipIfskipUnlessexpectedFailure装饰器,用于函数和类

  • TestCase.skipTest用于在测试内部进行命令式跳过

目前不支持以下习语:

pytest-xdist的惊喜

如果您决定在测试套件中使用pytest-xdist,请注意它会以任意顺序运行测试:每个工作进程将在完成其他测试后运行测试,因此测试执行的顺序是不可预测的。因为默认的unittest运行程序会按顺序顺序运行测试,并且通常以相同的顺序运行,这将经常暴露出测试套件中的并发问题,例如,试图使用相同名称创建临时目录的测试。您应该将这视为修复潜在并发问题的机会,因为它们本来就不应该是测试套件的一部分。

unittest 子类中的 pytest 特性

尽管不是设计为在运行基于unittest的测试时支持所有其特性,但是支持一些 pytest 习语:

  • 普通断言:当子类化unittest.TestCase时,pytest 断言内省的工作方式与之前一样

  • 标记:标记可以正常应用于unittest测试方法和类。处理标记的插件在大多数情况下应该正常工作(例如pytest-timeout标记)

  • 自动使用固定装置:在模块或conftest.py文件中定义的自动使用固定装置将在正常执行unittest测试方法时创建/销毁,包括在类范围的自动使用固定装置的情况下

  • 测试选择:命令行中的-k-m应该像正常一样工作

其他 pytest 特性与unittest不兼容,特别是:

  • 固定装置unittest测试方法无法请求固定装置。Pytest 使用unittest自己的结果收集器来执行测试,该收集器不支持向测试函数传递参数

  • 参数化:由于与固定装置的原因相似,这也不受支持:我们需要传递参数化值,目前这是不可能的。

不依赖于固定装置的插件可能会正常工作,例如pytest-timeoutpytest-randomly

使用 unitest2pytest 转换断言

一旦您将测试运行程序更改为 pytest,您就可以利用编写普通的断言语句来代替self.assert*方法。

转换所有的方法调用是无聊且容易出错的,这就是unittest2pytest工具存在的原因。它将所有的self.assert*方法调用转换为普通的断言,并将self.assertRaises调用转换为适当的 pytest 习语。

使用pip安装它:

λ pip install unittest2pytest

安装完成后,您现在可以在想要的文件上执行它:

λ unittest2pytest test_simple2.py
RefactoringTool: Refactored test_simple2.py
--- test_simple2.py (original)
+++ test_simple2.py (refactored)
@@ -5,6 +5,7 @@
 import unittest
 from collections import namedtuple
 from pathlib import Path
+import pytest

 DATA = """
 Main Grid,48,44
@@ -49,12 +50,12 @@
 self.grids = list(iter_grids_from_csv(self.filepath))

 def test_read_properties(self):
-        self.assertEqual(self.grids[0], GridData("Main Grid", 48, 44))
-        self.assertEqual(self.grids[1], GridData("2nd Grid", 24, 21))
-        self.assertEqual(self.grids[2], GridData("3rd Grid", 24, 48))
+        assert self.grids[0] == GridData("Main Grid", 48, 44)
+        assert self.grids[1] == GridData("2nd Grid", 24, 21)
+        assert self.grids[2] == GridData("3rd Grid", 24, 48)

 def test_invalid_path(self):
-        with self.assertRaises(IOError):
+        with pytest.raises(IOError):
 list(iter_grids_from_csv(Path("invalid file")))

 @unittest.expectedFailure
RefactoringTool: Files that need to be modified:
RefactoringTool: test_simple2.py

默认情况下,它不会触及文件,只会显示它可以应用的更改的差异。要实际应用更改,请传递-wn--write--nobackups)。

请注意,在上一个示例中,它正确地替换了self.assert*调用,self.assertRaises,并添加了pytest导入。它没有更改我们测试类的子类,因为这可能会有其他后果,具体取决于您正在使用的实际子类,因此unittest2pytest会保持不变。

更新后的文件运行方式与以前一样:

λ pytest test_simple2.py
======================== test session starts ========================
...
collected 3 items

test_simple2.py ..x                                            [100%]

================ 2 passed, 1 xfailed in 0.10 seconds ================

采用 pytest 作为运行程序,并能够使用普通的断言语句是一个经常被低估的巨大收获:不再需要一直输入self.assert...是一种解放。

在撰写本文时,unittest2pytest尚未处理最后一个测试中的self.fail("not implemented yet")语句。因此,我们需要手动用assert 0, "not implemented yet"替换它。也许您想提交一个 PR 来改进这个项目?(github.com/pytest-dev/unittest2pytest)。

处理设置/拆卸

要完全将TestCase子类转换为 pytest 风格,我们需要用 pytest 的习语替换unittest。我们已经在上一节中看到了如何使用unittest2pytest来做到这一点。但是我们能对setUptearDown方法做些什么呢?

正如我们之前学到的,TestCase子类中的autouse fixtures 工作得很好,所以它们是替换setUptearDown方法的一种自然方式。让我们使用上一节的例子。

在转换assert语句之后,首先要做的是删除unittest.TestCase的子类化:

class Test(unittest.TestCase):
    ...

这变成了以下内容:

class Test:
    ...

接下来,我们需要将setup/teardown方法转换为 fixture 等效方法:

    @classmethod
    def setUpClass(cls):
        cls.temp_dir = Path(tempfile.mkdtemp())
        cls.filepath = cls.temp_dir / "data.csv"
        cls.filepath.write_text(DATA.strip())

    @classmethod
    def tearDownClass(cls):
        shutil.rmtree(cls.temp_dir)

因此,类作用域的setUpClasstearDownClass方法将成为一个单一的类作用域 fixture:

    @classmethod
    @pytest.fixture(scope='class', autouse=True)
    def _setup_class(cls):
        temp_dir = Path(tempfile.mkdtemp())
        cls.filepath = temp_dir / "data.csv"
        cls.filepath.write_text(DATA.strip())
        yield
        shutil.rmtree(temp_dir)

由于yield语句,我们可以很容易地在 fixture 本身中编写拆卸代码,就像我们已经学到的那样。

以下是一些观察:

  • Pytest 不在乎我们如何称呼我们的 fixture,所以我们可以继续使用旧的setUpClass名称。我们选择将其更改为setup_class,有两个目标:避免混淆这段代码的读者,因为它可能看起来仍然是一个TestCase子类,并且使用_前缀表示这个 fixture 不应该像普通的 pytest fixture 一样使用。

  • 我们将temp_dir更改为局部变量,因为我们不再需要在cls中保留它。以前,我们不得不这样做,因为我们需要在tearDownClass期间访问cls.temp_dir,但现在我们可以将其保留为一个局部变量,并在yield语句之后访问它。这是使用yield将设置和拆卸代码分开的美妙之一:你不需要保留上下文变量;它们自然地作为函数的局部变量保留。

我们使用相同的方法来处理setUp方法:

    def setUp(self):
        self.grids = list(iter_grids_from_csv(self.filepath))

这变成了以下内容:

    @pytest.fixture(autouse=True)
    def _setup(self):
        self.grids = list(iter_grids_from_csv(self.filepath))

这种技术非常有用,因为你可以通过一组最小的更改得到一个纯粹的 pytest 类。此外,像我们之前做的那样为 fixtures 使用命名约定,有助于向读者传达 fixtures 正在转换旧的setup/teardown习惯。

现在这个类是一个合适的 pytest 类,你可以自由地使用 fixtures 和参数化。

管理测试层次结构

正如我们所看到的,在大型测试套件中需要共享功能是很常见的。由于unittest是基于子类化TestCase,所以在TestCase子类本身中放置额外的功能是很常见的。例如,如果我们需要测试需要数据库的应用逻辑,我们可能最初会直接在我们的TestCase子类中添加启动和连接到数据库的功能:

class Test(unittest.TestCase):

    def setUp(self):
        self.db_file = self.create_temporary_db()
        self.session = self.connect_db(self.db_file)

    def tearDown(self):
        self.session.close()
        os.remove(self.db_file)

    def create_temporary_db(self):
        ...

    def connect_db(self, db_file):
        ...

    def create_table(self, table_name, **fields):
        ...

    def check_row(self, table_name, **query):
        ...

    def test1(self):
        self.create_table("weapons", name=str, type=str, dmg=int)
        ...

这对于单个测试模块效果很好,但通常情况下,我们需要在以后的某个时候在另一个测试模块中使用这个功能。unittest模块没有内置的功能来共享常见的setup/teardown代码,所以大多数人自然而然地会将所需的功能提取到一个超类中,然后在需要的地方从中创建一个子类:

# content of testing.py
class DataBaseTesting(unittest.TestCase):

    def setUp(self):
        self.db_file = self.create_temporary_db()
        self.session = self.connect_db(self.db_file)

    def tearDown(self):
        self.session.close()
        os.remove(self.db_file)

    def create_temporary_db(self):
        ...

    def connect_db(self, db_file):
        ...

    def create_table(self, table_name, **fields):
        ...

    def check_row(self, table_name, **query):
        ...

# content of test_database2.py
from . import testing

class Test(testing.DataBaseTesting):

    def test1(self):
        self.create_table("weapons", name=str, type=str, dmg=int)
        ...

超类通常不仅包含setup/teardown代码,而且通常还包括调用self.assert*执行常见检查的实用函数(例如在上一个例子中的check_row)。

继续我们的例子:一段时间后,我们需要在另一个测试模块中完全不同的功能,例如,测试一个 GUI 应用程序。我们现在更加明智,怀疑我们将需要在几个其他测试模块中使用 GUI 相关的功能,所以我们首先创建一个具有我们直接需要的功能的超类:

class GUITesting(unittest.TestCase):

    def setUp(self):
        self.app = self.create_app()

    def tearDown(self):
        self.app.close_all_windows()

    def mouse_click(self, window, button):
        ...

    def enter_text(self, window, text):
        ...

setup/teardown和测试功能移动到超类的方法是可以的,并且易于理解。

当我们需要在同一个测试模块中使用两个不相关的功能时,问题就出现了。在这种情况下,我们别无选择,只能求助于多重继承。假设我们需要测试连接到数据库的对话框;我们将需要编写这样的代码:

from . import testing

class Test(testing.DataBaseTesting, testing.GUITesting):

    def setUp(self):
 testing.DataBaseTesting.setUp(self)
 testing.GUITesting.setUp(self)

    def tearDown(self):
 testing.GUITesting.setUp(self)
 testing.DataBaseTesting.setUp(self)

一般来说,多重继承会使代码变得不太可读,更难以理解。在这里,它还有一个额外的恼人之处,就是我们需要显式地按正确的顺序调用setUptearDown

还要注意的一点是,在 unittest 框架中,setUptearDown 是可选的,因此如果某个类不需要任何拆卸代码,通常不会声明 tearDown 方法。如果此类包含的功能后来移动到超类中,许多子类可能也不会声明 tearDown 方法。问题出现在后来的多重继承场景中,当您改进超类并需要添加 tearDown 方法时,因为现在您必须检查所有子类,并确保它们调用超类的 tearDown 方法。

因此,假设我们发现自己处于前述情况,并且希望开始使用与 TestCase 测试不兼容的 pytest 功能。我们如何重构我们的实用类,以便我们可以自然地从 pytest 中使用它们,并且保持现有的基于 unittest 的测试正常工作?

使用 fixtures 重用测试代码

我们应该做的第一件事是将所需的功能提取到定义良好的 fixtures 中,并将它们放入 conftest.py 文件中。继续我们的例子,我们可以创建 db_testinggui_testing fixtures:

class DataBaseFixture:

    def __init__(self):
        self.db_file = self.create_temporary_db()
        self.session = self.connect_db(self.db_file)

    def teardown(self):
        self.session.close()
        os.remove(self.db_file)

    def create_temporary_db(self):
        ...

    def connect_db(self, db_file):
        ...

    ...

@pytest.fixture
def db_testing():
    fixture = DataBaseFixture()
    yield fixture
    fixture.teardown()

class GUIFixture:

    def __init__(self):
        self.app = self.create_app()

    def teardown(self):
        self.app.close_all_windows()

    def mouse_click(self, window, button):
        ...

    def enter_text(self, window, text):
        ...

@pytest.fixture
def gui_testing():
    fixture = GUIFixture()
    yield fixture
    fixture.teardown()

现在,您可以开始使用纯 pytest 风格编写新的测试,并使用 db_testinggui_testing fixtures,这很棒,因为它为在新测试中使用 pytest 功能打开了大门。但这里很酷的一点是,我们现在可以更改 DataBaseTestingGUITesting 来重用 fixtures 提供的功能,而不会破坏现有代码:

class DataBaseTesting(unittest.TestCase):

    @pytest.fixture(autouse=True)
    def _setup(self, db_testing):
 self._db_testing = db_testing

    def create_temporary_db(self):
        return self._db_testing.create_temporary_db()

    def connect_db(self, db_file):
        return self._db_testing.connect_db(db_file)

    ...

class GUITesting(unittest.TestCase):

    @pytest.fixture(autouse=True)
 def _setup(self, gui_testing):
 self._gui_testing = gui_testing

    def mouse_click(self, window, button):
        return self._gui_testing.mouse_click(window, button)

    ...

我们的 DatabaseTestingGUITesting 类通过声明一个自动使用的 _setup fixture 来获取 fixture 值,这是我们在本章早期学到的一个技巧。我们可以摆脱 tearDown 方法,因为 fixture 将在每次测试后自行清理,而实用方法变成了在 fixture 中实现的方法的简单代理。

作为奖励分,GUIFixtureDataBaseFixture 也可以使用其他 pytest fixtures。例如,我们可能可以移除 DataBaseTesting.create_temporary_db(),并使用内置的 tmpdir fixture 为我们创建临时数据库文件:

class DataBaseFixture:

    def __init__(self, tmpdir):
        self.db_file = str(tmpdir / "file.db")
        self.session = self.connect_db(self.db_file)

    def teardown(self):
        self.session.close()

    ...

@pytest.fixture
def db_testing(tmpdir):
    fixture = DataBaseFixture(tmpdir)
    yield fixture
    fixture.teardown()

然后使用其他 fixtures 可以极大地简化现有的测试实用程序代码。

值得强调的是,这种重构不需要对现有测试进行任何更改。这里,fixtures 的一个好处再次显而易见:fixture 的要求变化不会影响使用 fixture 的测试。

重构测试实用程序

在前一节中,我们看到测试套件可能使用子类来共享测试功能,并且如何将它们重构为 fixtures,同时保持现有的测试正常工作。

unittest 套件中通过超类共享测试功能的另一种选择是编写单独的实用类,并在测试中使用它们。回到我们的例子,我们需要具有与数据库相关的设施,这是一种在 unittest 友好的方式实现的方法,而不使用超类:

# content of testing.py
class DataBaseTesting:

    def __init__(self, test_case):        
        self.db_file = self.create_temporary_db()
        self.session = self.connect_db(self.db_file)
        self.test_case = test_case
        test_case.addCleanup(self.teardown)

    def teardown(self):
        self.session.close()
        os.remove(self.db_file)

    ...

    def check_row(self, table_name, **query):
        row = self.session.find(table_name, **query)
        self.test_case.assertIsNotNone(row)
        ...

# content of test_1.py
from testing import DataBaseTesting

class Test(unittest.TestCase):

    def test_1(self):
        db_testing = DataBaseTesting(self)
        db_testing.create_table("weapons", name=str, type=str, dmg=int)
        db_testing.check_row("weapons", name="zweihander")
        ...

在这种方法中,我们将测试功能分离到一个类中,该类将当前的 TestCase 实例作为第一个参数,然后是任何其他所需的参数。

TestCase实例有两个目的:为类提供对各种self.assert*函数的访问,并作为一种方式向TestCase.addCleanup注册清理函数(docs.python.org/3/library/unittest.html#unittest.TestCase.addCleanup)。TestCase.addCleanup注册的函数将在每个测试完成后调用,无论它们是否成功。我认为它们是setUp/tearDown函数的一个更好的替代方案,因为它们允许资源被创建并立即注册进行清理。在setUp期间创建所有资源并在tearDown期间释放它们的缺点是,如果在setUp方法中引发任何异常,那么tearDown将根本不会被调用,从而泄漏资源和状态,这可能会影响后续的测试。

如果您的unittest套件使用这种方法进行测试设施,那么好消息是,您可以轻松地转换/重用这些功能以供 pytest 使用。

因为这种方法与 fixtures 的工作方式非常相似,所以很容易稍微改变类以使其作为 fixtures 工作:

# content of testing.py
class DataBaseFixture:

    def __init__(self):
        self.db_file = self.create_temporary_db()
        self.session = self.connect_db(self.db_file)

    ...

    def check_row(self, table_name, **query):
        row = self.session.find(table_name, **query)
        assert row is not None

# content of conftest.py
@pytest.fixture
def db_testing():
    from .testing import DataBaseFixture
    result = DataBaseFixture()
    yield result
    result.teardown()

我们摆脱了对TestCase实例的依赖,因为我们的 fixture 现在负责调用teardown(),并且我们可以自由地使用普通的 asserts 而不是Test.assert*方法。

为了保持现有的套件正常工作,我们只需要创建一个薄的子类来处理在与TestCase子类一起使用时的清理:

# content of testing.py
class DataBaseTesting(DataBaseFixture):

    def __init__(self, test_case):
        super().__init__()
        test_case.addCleanup(self.teardown) 

通过这种小的重构,我们现在可以在新测试中使用原生的 pytest fixtures,同时保持现有的测试与以前完全相同的工作方式。

虽然这种方法效果很好,但一个问题是,不幸的是,我们无法在DataBaseFixture类中使用其他 pytest fixtures(例如tmpdir),而不破坏在TestCase子类中使用DataBaseTesting的兼容性。

迁移策略

能够立即使用 pytest 作为运行器开始使用unittest-based 测试绝对是一个非常强大的功能。

最终,您需要决定如何处理现有的基于unittest的测试。您可以选择几种方法:

  • 转换所有内容:如果您的测试套件相对较小,您可能决定一次性转换所有测试。这样做的好处是,您不必妥协以保持现有的unittest套件正常工作,并且更容易被他人审查,因为您的拉取请求将具有单一主题。

  • 边转换边进行:您可能决定根据需要转换测试和功能。当您需要添加新测试或更改现有测试时,您可以利用这个机会转换测试和/或重构功能,使用前几节中的技术来创建 fixtures。如果您不想花时间一次性转换所有内容,而是慢慢地铺平道路,使 pytest 成为唯一的测试套件,那么这是一个很好的方法。

  • 仅新测试:您可能决定永远不触及现有的unittest套件,只在 pytest 风格中编写新测试。如果您有成千上万的测试,可能永远不需要进行维护,那么这种方法是合理的,但您将不得不保持前几节中展示的混合方法永远正常工作。

根据您的时间预算和测试套件的大小选择要使用的迁移策略。

总结

我们已经讨论了一些关于如何在各种规模的基于unittest的测试套件中使用 pytest 的策略和技巧。我们从讨论如何使用 pytest 作为测试运行器开始,以及哪些功能适用于TestCase测试。我们看了看如何使用unittest2pytest工具将self.assert*方法转换为普通的 assert 语句,并充分利用 pytest 的内省功能。然后,我们学习了一些关于如何将基于unittestsetUp/tearDown代码迁移到 pytest 风格的测试类中的技巧,管理在测试层次结构中分散的功能,以及一般的实用工具。最后,我们总结了可能的迁移策略概述,适用于各种规模的测试套件。

在下一章中,我们将简要总结本书学到的内容,并讨论接下来可能会有什么。

标签:指南,启动,self,fixture,pytest,PyTest,测试,test,def
From: https://www.cnblogs.com/apachecn/p/18140422

相关文章

  • Python-GPU-编程实用指南(一)
    PythonGPU编程实用指南(一)原文:zh.annas-archive.org/md5/ef7eb3c148e0cfdfe01c331f2f01557c译者:飞龙协议:CCBY-NC-SA4.0前言问候和祝福!本文是关于使用Python和CUDA进行GPU编程的入门指南。GPU可能代表图形编程单元,但我们应该明确,这本书不是关于图形编程——它本质......
  • Python-GPU-编程实用指南(三)
    PythonGPU编程实用指南(三)原文:zh.annas-archive.org/md5/ef7eb3c148e0cfdfe01c331f2f01557c译者:飞龙协议:CCBY-NC-SA4.0第十章:使用已编译的GPU代码在本书的过程中,我们通常依赖PyCUDA库自动为我们接口我们的内联CUDA-C代码,使用即时编译和与Python代码的链接。然而......
  • Python-企业自动化实用指南(四)
    Python企业自动化实用指南(四)原文:zh.annas-archive.org/md5/0bfb2f4dbc80a06d99550674abb53d0d译者:飞龙协议:CCBY-NC-SA4.0第十八章:使用Python构建网络扫描器在本章中,我们将构建一个网络扫描器,它可以识别网络上的活动主机,并且我们还将扩展它以包括猜测每个主机上正在运......
  • Python 数据结构和算法实用指南(三)
    原文:zh.annas-archive.org/md5/66ae3d5970b9b38c5ad770b42fec806d译者:飞龙协议:CCBY-NC-SA4.0第七章:哈希和符号表我们之前已经看过数组和列表,其中项目按顺序存储并通过索引号访问。索引号对计算机来说很有效。它们是整数,因此快速且易于操作。但是,它们并不总是对我们很有效......
  • Python 数据结构和算法实用指南(一)
    原文:zh.annas-archive.org/md5/66ae3d5970b9b38c5ad770b42fec806d译者:飞龙协议:CCBY-NC-SA4.0前言数据结构和算法是信息技术和计算机科学工程学习中最重要的核心学科之一。本书旨在提供数据结构和算法的深入知识,以及编程实现经验。它专为初学者和中级水平的研究Python编......
  • Python 数据结构和算法实用指南(二)
    原文:zh.annas-archive.org/md5/66ae3d5970b9b38c5ad770b42fec806d译者:飞龙协议:CCBY-NC-SA4.0第四章:列表和指针结构我们已经在Python中讨论了列表,它们方便而强大。通常情况下,我们使用Python内置的列表实现来存储任何数据。然而,在本章中,我们将了解列表的工作原理,并将研......
  • step by step系列之:openGauss1.0.1单机安装指南v1.2
    StepbyStep之:openGauss1.0.1单机安装指南v1.2在CentOS7.6上安装openGauss单机版配置操作系统满足安装要求硬件环境:虚拟机的内存8GB,4核心CPU,900G磁盘(非必须)软件环境:CentOS7.6关闭防火墙停止firewallsystemctlstopfirewalld.service禁止firewall开机启动......
  • bootmgfw.efi 是 Windows 操作系统中的一个关键文件,它是用于启动 UEFI(统一扩展固件接
    bootmgfw.efi是Windows操作系统中的一个关键文件,它是用于启动UEFI(统一扩展固件接口)计算机的WindowsBootManager。这个文件通常位于Windows安装的EFI系统分区(ESP)中的\EFI\Microsoft\Boot\目录下。在UEFI计算机上,bootmgfw.efi负责加载Windows操作系统的启动程......
  • 宝塔面板mysql无法启动问题如何解决
    宝塔面板无法启动的问题和解决如果你的宝塔里面的mysql无法启动了,请先看是不是以下的配置问题1.是不是你的3306端口被占用了导致mysql无法启动2.是不是磁盘空间不足导致的无法启动如果都不是这些问题再继续向下看常见问题:1、Mysql安装好后或迁移文件后无法启动2、Mysql异常......
  • springboot多模块项目启动经历
    springboot多模块使用@目录springboot多模块使用前言大佬把项目权限给我了,我就先下下来看看学习一下一、识别二、maven配置1.安装maven三、加载刷新总结前言大佬把项目权限给我了,我就先下下来看看学习一下一、识别项目分为母模块和多个子模块,开始idea只是识别了最外层的pom......