将(yield)fixture作为测试参数传递(使用临时目录)

时间:2017-07-20 22:04:59

标签: python pytest

问题

是否可以将屈服pytest灯具(用于设置和拆卸)作为参数传递给测试功能?

上下文

我正在测试一个对象,该对象从/向单个目录中的文件读取和写入数据。该目录的路径将保存为对象的属性。

我无法实现以下两个目标:

  1. 在我的测试中使用临时目录;和
  2. 确保在每次测试后删除目录。
  3. 实施例

    考虑以下内容(test_yieldfixtures.py):

    import pytest, tempfile, os, shutil
    from contextlib import contextmanager
    
    @contextmanager
    def data():
        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():
        with data() as datadir:
            errorfile = os.path.join(datadir, 'testlog1.log')
            yield Thing(datadir=datadir, errorfile=errorfile)
    
    @pytest.fixture
    def thing2():
        with data() as datadir:
            errorfile = os.path.join(datadir, 'testlog2.log')
            yield Thing(datadir=datadir, errorfile=errorfile)
    
    @pytest.mark.parametrize('thing', [thing1, thing2])
    def test_attr(thing):
        print(thing.datadir)
        assert os.path.exists(thing.datadir)
    

    运行pytest test_yieldfixtures.py会输出以下内容:

    ================================== 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
    

    行。因此夹具功能没有我的类的属性。很公平。

    尝试1

    一个函数没有属性,所以我试着调用那些函数来实际获取对象。但是,那只是

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

    结果:

    ================================== 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

    我还尝试在return灯具中使用yield代替thing1/2,但这会将我踢出data上下文管理器并删除目录:

    ================================== 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)
    

    重申问题:无论如何都要将这些灯具作为参数传递并维护临时目录的清理?

3 个答案:

答案 0 :(得分:2)

临时目录和文件由pytest使用内置灯具 tmpdir tmpdir_factory 进行处理。

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

此外,参数化灯具适用于此示例 这些记录在此处:https://docs.pytest.org/en/latest/fixture.html#fixture-parametrize

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,在Python Testing with pytest中,参数化的灯具在ch3中有所涉及。 ch4中包含了tmpdir和其他内置灯具。

答案 1 :(得分:1)

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

你的函数thing1和thing2包含yield语句。当你调用这样的函数时,返回的值是一个&#34;生成器对象。&#34;它是一个迭代器 - 一系列值,当然与yield的第一个值或任何一个特定值不同。

这些是传递给test_attr函数的对象。测试环境正在为您自动完成,或者至少我认为它是如何工作的。

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

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

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)

但这只是第一个障碍。生成器功能未完成。它的内部&#34;程序计数器&#34;就在yield陈述之后。所以你还没有离开上下文管理器,还没有删除你的临时目录。为此,您必须再次致电next(thing)并抓住StopIteration例外。

或者我认为这样可行:

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

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

对我而言,这是一个悬而未决的问题,这是否会使您的代码具有或多或少的可读性和可维护性。它有点笨拙。

答案 2 :(得分:1)

尝试将data函数/生成器放入夹具中。然后使用request.getfixturevalue()动态运行命名的fixture。

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灯具:

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)