pytest和基于产量的测试

时间:2017-03-08 16:26:49

标签: python pytest nose

我正在尝试将一堆测试从nose迁移到pytest,我无法迁移一个验证整个过程的测试。

我把它愚蠢地代表我的问题:

def is_equal(a, b):
    assert a == b


def inner():
    yield is_equal, 2, 2
    yield is_equal, 3, 3


def test_simple():
    yield is_equal, 0, 0
    yield is_equal, 1, 1
    for test in inner():
        yield test
    yield is_equal, 4, 4
    yield is_equal, 5, 5


def test_complex():

    integers = list()

    def update_integers():
        integers.extend([0, 1, 2, 3, 4, 5])

    yield update_integers

    for x in integers:
        yield is_equal, x, x

test_simple在nose和pytest之间运行正常,但test_complex只运行初始的update_integers测试:

~/projects/testbox$ nosetests -v
test_nose_tests.test_simple(0, 0) ... ok
test_nose_tests.test_simple(1, 1) ... ok
test_nose_tests.test_simple(2, 2) ... ok
test_nose_tests.test_simple(3, 3) ... ok
test_nose_tests.test_simple(4, 4) ... ok
test_nose_tests.test_simple(5, 5) ... ok
test_nose_tests.test_complex ... ok
test_nose_tests.test_complex(0, 0) ... ok
test_nose_tests.test_complex(1, 1) ... ok
test_nose_tests.test_complex(2, 2) ... ok
test_nose_tests.test_complex(3, 3) ... ok
test_nose_tests.test_complex(4, 4) ... ok
test_nose_tests.test_complex(5, 5) ... ok

----------------------------------------------------------------------
Ran 13 tests in 0.004s


~/projects/testbox$ pytest -v
====================================================================     test session starts         =====================================================================
platform linux2 -- Python 2.7.12, pytest-3.0.6, py-1.4.32, pluggy-0.4.0 -- /usr/bin/python
cachedir: .cache
rootdir: /home/local/ANT/cladam/projects/testbox, inifile: 
collected 7 items 

tests/test_nose_tests.py::test_simple::[0] PASSED
tests/test_nose_tests.py::test_simple::[1] PASSED
tests/test_nose_tests.py::test_simple::[2] PASSED
tests/test_nose_tests.py::test_simple::[3] PASSED
tests/test_nose_tests.py::test_simple::[4] PASSED
tests/test_nose_tests.py::test_simple::[5] PASSED
tests/test_nose_tests.py::test_complex::[0] PASSED

=================================================================== pytest-warning summary     ===================================================================
WC1 /home/local/ANT/cladam/projects/testbox/tests/test_nose_tests.py yield tests are deprecated, and scheduled to be removed in pytest 4.0
....
======================================================== 7 passed, 7     pytest-warnings in 0.01 seconds =========================================================

我假设这是因为在收集时整数列表是空的,然后它不会收集6个额外的收益。

有没有办法在pytest中复制这个测试结构?通过pytest_generate_tests?

此测试代表了一系列更大的事件,用于构建对象并对其进行操作,并在过程的每个阶段进行测试。

  1. 建模
  2. 验证某些模型属性
  3. 基于模型创建和输出文件
  4. 针对已知输出进行差异以查看是否存在更改。
  5. 提前致谢

2 个答案:

答案 0 :(得分:2)

正如测试结果所示,不推荐使用基于产量的测试:

WC1 /home/local/ANT/cladam/projects/testbox/tests/test_nose_tests.py yield tests are deprecated, and scheduled to be removed in pytest 4.0

我建议您使用pytest.parametrize,您可以在此处查看更多相关内容:http://doc.pytest.org/en/latest/parametrize.html

从你的例子中我会为测试创建这样的东西:

def is_equal(a, b):
    return a == b


import pytest

class TestComplexScenario:
    @pytest.mark.parametrize("my_integer", [0, 1, 2])
    def test_complex(self, my_integer):
        assert is_equal(my_integer, my_integer)

这里有一个输出样本:

test_complex.py::TestComplexScenario::test_complex[0] PASSED
test_complex.py::TestComplexScenario::test_complex[1] PASSED
test_complex.py::TestComplexScenario::test_complex[2] PASSED

您可以在此处找到有关参数化的更多示例:http://layer0.authentise.com/pytest-and-parametrization.html

您还可以为测试输入进行排列,请在此处查看示例: pytest: parameterized test with cartesian product of arguments

答案 1 :(得分:2)

问题是pytest会在运行任何测试之前收集所有测试,因此在)内,test_complex函数在收集过程结束后才会被调用。

通过将update_integers检查从收集阶段移至测试运行阶段,您可以通过将is_generator中的以下内容放入测试来运行测试。不幸的是,钩子不允许conftest.py作为生成器运行,因此pytest-3.2.1的pytest_runtest_protocol的整个内容被复制和修改。

_pytest.main.pytest_runtestloop

如果自3.2.1版本以来pytest发生了太大的变化,上述情况可能无效。而是根据需要复制和修改最新版本的import pytest from _pytest.compat import is_generator def pytest_pycollect_makeitem(collector, name, obj): """ Override the collector so that generators are saved as functions to be run during the test phase rather than the collection phase. """ if collector.istestfunction(obj, name) and is_generator(obj): return [pytest.Function(name, collector, args=(), callobj=obj)] def pytest_runtestloop(session): """ Copy of _pytest.main.pytest_runtestloop with the session iteration modified to perform a subitem iteration. """ if (session.testsfailed and not session.config.option.continue_on_collection_errors): raise session.Interrupted( "%d errors during collection" % session.testsfailed) if session.config.option.collectonly: return True for i, item in enumerate(session.items): nextitem = session.items[i + 1] if i + 1 < len(session.items) else None # The new functionality is here: treat all items as if they # might have sub-items, and run through them one by one. for subitem in get_subitems(item): subitem.config.hook.pytest_runtest_protocol(item=subitem, nextitem=nextitem) if getattr(session, "shouldfail", False): raise session.Failed(session.shouldfail) if session.shouldstop: raise session.Interrupted(session.shouldstop) return True def get_subitems(item): """ Return a sequence of subitems for the given item. If the item is not a generator, then just yield the item itself as the sequence. """ if not isinstance(item, pytest.Function): yield item obj = item.obj if is_generator(obj): for number, yielded in enumerate(obj()): index, call, args = interpret_yielded_test(yielded, number) test = pytest.Function(item.name+index, item.parent, args=args, callobj=call) yield test else: yield item def interpret_yielded_test(obj, number): """ Process an item yielded from a generator. If the item is named, then set the index to "['name']", otherwise set it to "[number]". Return the index, the callable and the arguments to the callable. """ if not isinstance(obj, (tuple, list)): obj = (obj,) if not callable(obj[0]): index = "['%s']"%obj[0] obj = obj[1:] else: index = "[%d]"%number call, args = obj[0], obj[1:] return index, call, args ;这应该为您的项目提供时间逐渐远离基于产量的测试用例,或者至少可以在收集时收集基于产量的测试用例。