关于python:将(yield)夹具作为测试参数传递(带有temp目录)

Passing (yield) fixtures as test parameters (with a temp directory)

问题

是否可以将生成的pytest固定装置(用于设置和拆卸)作为参数传递给测试函数?

上下文

我正在测试一个对象,该对象在单个目录中读写文件。该目录的路径另存为对象的属性。

我在以下方面遇到了麻烦:

  • 在测试中使用临时目录;和
  • 确保在每次测试后都删除目录。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ================================== FAILURES ===================================
    ______________________________ test_attr[thing0] ______________________________

    thing = <generator object thing1 at 0x0000017B50C61BF8>

        @pytest.mark.parametrize('thing', [thing1, thing2])
        def test_attr(thing):
    >        print(thing.datadir)
    E       AttributeError: 'function' object has no attribute 'props'

    test_mod.py:39: AttributeError

    因此,fixture函数没有我的类的属性。足够公平。

    尝试1

    一个函数将没有属性,因此我尝试调用该函数以实际获取对象。但是,只是

    1
    2
    3
    4
    @pytest.mark.parametrize('thing', [thing1(), thing2()])
    def test_attr(thing):
        print(thing.props['datadir'])
        assert os.path.exists(thing.get('datadir'))

    的结果是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ================================== FAILURES ===================================
    ______________________________ test_attr[thing0] ______________________________

    thing = <generator object thing1 at 0x0000017B50C61BF8>

        @pytest.mark.parametrize('thing', [thing1(), thing2()])
        def test_attr(thing):
    >       print(thing.datadir)
    E       AttributeError: 'generator' object has no attribute 'props'

    test_mod.py:39: AttributeError

    尝试2

    我也尝试在thing1/2固定装置中使用return而不是yield,但这让我大吃一惊从data上下文管理器中移出并删除目录:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ================================== FAILURES ===================================
    ______________________________ test_attr[thing0] ______________________________

    thing = <test_mod.Thing object at 0x000001C528F05358>

        @pytest.mark.parametrize('thing', [thing1(), thing2()])
        def test_attr(thing):
            print(thing.datadir)
    >       assert os.path.exists(thing.datadir)

    Closing

    重述问题:无论如何,是否有将这些固定装置作为参数传递并维护临时目录的清理?


    尝试将您的data函数/生成器制作为灯具。然后使用request.getfixturevalue()动态运行命名的灯具。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import pytest, tempfile, os, shutil
    from contextlib import contextmanager

    @pytest.fixture # This works with pytest>3.0, on pytest<3.0 use yield_fixture
    def datadir():
        datadir = tempfile.mkdtemp()  # setup
        yield datadir
        shutil.rmtree(datadir)        # teardown

    class Thing:
        def __init__(self, datadir, errorfile):
            self.datadir = datadir
            self.errorfile = errorfile


    @pytest.fixture
    def thing1(datadir):
        errorfile = os.path.join(datadir, 'testlog1.log')
        yield Thing(datadir=datadir, errorfile=errorfile)

    @pytest.fixture
    def thing2(datadir):
        errorfile = os.path.join(datadir, 'testlog2.log')
        yield Thing(datadir=datadir, errorfile=errorfile)

    @pytest.mark.parametrize('thing_fixture_name', ['thing1', 'thing2'])
    def test_attr(request, thing):
        thing = request.getfixturevalue(thing) # This works with pytest>3.0, on pytest<3.0 use getfuncargvalue
        print(thing.datadir)
        assert os.path.exists(thing.datadir)

    再进一步,您可以对thing灯具进行参数设置,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Thing:
        def __init__(self, datadir, errorfile):
            self.datadir = datadir
            self.errorfile = errorfile

    @pytest.fixture(params=['test1.log', 'test2.log'])
    def thing(request):
        with tempfile.TemporaryDirectory() as datadir:
            errorfile = os.path.join(datadir, request.param)
            yield Thing(datadir=datadir, errorfile=errorfile)

    def test_thing_datadir(thing):
        assert os.path.exists(thing.datadir)


    临时目录和文件由pytest使用内置的夹具tmpdir和tmpdir_factory处理。

    对于这种用法,tmpdir应该足够了:https://docs.pytest.org/en/latest/tmpdir.html

    此外,在此示例中,经过参数化的灯具也可以很好地工作。记录在这里:https://docs.pytest.org/en/latest/fixture.html#fixture-parametrize

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import os
    import pytest


    class Thing:
        def __init__(self, datadir, errorfile):
            self.datadir = datadir
            self.errorfile = errorfile


    @pytest.fixture(params=(1, 2))
    def thing(request, tmpdir):
        errorfile_name = 'testlog{}.log'.format(request.param)
        errorfile = tmpdir.join(errorfile_name)
        return Thing(datadir=str(tmpdir), errorfile=str(errorfile))


    def test_attr(request, thing):
        assert os.path.exists(thing.datadir)

    BTW,在pytest的Python测试中,ch3涵盖了参数化的灯具。 ch4中涵盖了tmpdir和其他内置固定装置。


    我看到了您的问题,但不确定解决方案。问题:

    您的函数thing1和thing2包含yield语句。当您调用这样的函数时,返回的值是一个" generator object。"。它是一个迭代器-一系列值,这当然与yield的第一个值或任何一个特定值不同值。

    这些是传递给test_attr函数的对象。测试环境会自动为您做到这一点,或者至少我认为这就是它的工作方式。

    您真正想要的是在yield表达式中创建的对象,换句话说,是Thing(datadir=datadir, errorfile=errorfile)。有三种方法可以使生成器发出其各个值:通过调用next(iter),通过调用iter.__next__()或在带有in表达式的循环中使用迭代器。

    一种可能性是迭代生成器一次。像这样:

    1
    2
    3
    4
    def test_attr(thing):
        first_thing = next(thing)
        print(first_thing.datadir)
        assert os.path.exists(first_thing.datadir)

    first_thing将是您要测试的对象,即Thing(datadir=datadir, errorfile=errorfile)

    但这只是第一个障碍。生成器功能未完成。其内部的"程序计数器"位于yield语句之后。因此,您尚未退出上下文管理器,也尚未删除您的临时目录。为此,您必须再次调用next(thing)并捕获一个StopIteration异常。

    或者我认为这会起作用:

    1
    2
    3
    4
    def test_attr(thing):
        for a_thing in thing:
            print(a_thing.datadir)
            assert os.path.exists(a_thing.datadir)

    in表达式遍历所有项在迭代器中(只有一个),并在StopIteration发生时正常退出。该函数从上下文管理器中退出,您的工作已完成。

    对我来说,这是否使您的代码或多或少地易于阅读和可维护是一个悬而未决的问题。有点笨拙。