diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index 2ddee3b4d..2db765422 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -22,12 +22,12 @@ more "templaty" and more test-aspect oriented. In fact, funcarg mechanisms are meant to be complete and convenient enough to -* substitute most usages of `xUnit style`_ setup. +* substitute and improve on most usages of `xUnit style`_ setup. For a simple example of how funcargs compare to xUnit setup, see the `blog post about the monkeypatch funcarg`_. -* substitute all usages of `old-style generative tests`_, +* substitute and improve on all usages of `old-style generative tests`_, i.e. test functions that use the "yield" statement. Using yield in test functions is deprecated since 1.0. @@ -95,7 +95,7 @@ Note that if you misspell a function argument or want to use one that isn't available, an error with a list of available function argument is provided. -For provider functions that make good use of the +For more interesting provider functions that make good use of the `request object`_ please see the `application setup tutorial example`_. .. _`request object`: @@ -164,9 +164,9 @@ for a use of this method. generating parametrized tests with funcargs =========================================================== -You can parametrize multiple runs of the same test function -by schedulings new test function calls which get different -funcarg values. Let's look at a simple self-contained +You can directly parametrize multiple runs of the same test +function by adding new test function calls with different +function argument values. Let's look at a simple self-contained example: .. sourcecode:: python @@ -175,10 +175,7 @@ example: def pytest_generate_tests(metafunc): if "numiter" in metafunc.funcargnames: for i in range(10): - metafunc.addcall(param=i) - - def pytest_funcarg__numiter(request): - return request.param + metafunc.addcall(funcargs=dict(numiter=i)) def test_func(numiter): assert numiter < 9 @@ -208,16 +205,9 @@ If you run this with ``py.test test_example.py`` you'll get: Here is what happens in detail: 1. ``pytest_generate_tests(metafunc)`` hook is called once for each test - function. ``metafunc.addcall(param=i)`` adds new test function calls - where the ``param`` will appear as ``request.param``. + function. It adds ten new function calls with explicit function arguments. -2. the ``pytest_funcarg__arg1(request)`` provider - is called 10 times. Each time it receives a request object - that has a ``request.param`` as previously provided by the generator. - Our provider here simply passes through the ``param`` value. - We could also setup more heavyweight resources here. - -3. **execute tests**: ``test_func(numiter)`` is called ten times with +2. **execute tests**: ``test_func(numiter)`` is called ten times with ten different arguments. .. _`metafunc object`: @@ -246,11 +236,11 @@ the ``metafunc.addcall()`` method .. sourcecode:: python - def addcall(id=None, param=None): - """ trigger a later test function call. """ + def addcall(funcargs={}, id=None, param=None): + """ trigger a new test function call. """ -The specified ``param`` will be seen by the -`funcarg provider`_ as a ``request.param`` attribute. +``funcargs`` can be a dictionary of argument names +mapped to values - providing it is called *direct parametrization*. If you provide an `id`` it will be used for reporting and identification purposes. If you don't supply an `id` @@ -258,11 +248,16 @@ the stringified counter of the list of added calls will be used. ``id`` values needs to be unique between all invocations for a given test function. -*Test generators are called during test collection which -is separate from the actual test setup and test run. -With distributed testing setting up funcargs will -even happen in a different process. Therefore one should -defer setup of heavyweight objects to funcarg providers.* +``param`` if specified will be seen by any +`funcarg provider`_ as a ``request.param`` attribute. +Setting it is called *indirect parametrization*. + +Indirect parametrization is preferable if test values are +expensive to setup or can only be created in certain environments. +Test generators and thus ``addcall()`` invocations are performed +during test collection which is separate from the actual test +setup and test run phase. With distributed testing collection +and test setup/run happens in different process. .. _`tutorial examples`: diff --git a/py/test/dist/testing/test_mypickle.py b/py/test/dist/testing/test_mypickle.py index 2a9caade3..0455b7255 100644 --- a/py/test/dist/testing/test_mypickle.py +++ b/py/test/dist/testing/test_mypickle.py @@ -4,22 +4,24 @@ from py.__.test.dist.mypickle import ImmutablePickler, PickleChannel, UnpickleEr class A: pass -def test_pickle_and_back_IS_same(): - def pickle_band_back_IS_same(obj, proto): - p1 = ImmutablePickler(uneven=False, protocol=proto) - p2 = ImmutablePickler(uneven=True, protocol=proto) - s1 = p1.dumps(obj) - d2 = p2.loads(s1) - s2 = p2.dumps(d2) - obj_back = p1.loads(s2) - assert obj is obj_back - a1 = A() - a2 = A() - a2.a1 = a1 - for proto in (0,1,2, -1): - for obj in {1:2}, [1,2,3], a1, a2: - yield pickle_band_back_IS_same, obj, proto +def pytest_generate_tests(metafunc): + if "obj" in metafunc.funcargnames and "proto" in metafunc.funcargnames: + a1 = A() + a2 = A() + a2.a1 = a1 + for proto in (0,1,2, -1): + for obj in {1:2}, [1,2,3], a1, a2: + metafunc.addcall(funcargs=dict(obj=obj, proto=proto)) + +def test_pickle_and_back_IS_same(obj, proto): + p1 = ImmutablePickler(uneven=False, protocol=proto) + p2 = ImmutablePickler(uneven=True, protocol=proto) + s1 = p1.dumps(obj) + d2 = p2.loads(s1) + s2 = p2.dumps(d2) + obj_back = p1.loads(s2) + assert obj is obj_back def test_pickling_twice_before_unpickling(): p1 = ImmutablePickler(uneven=False) diff --git a/py/test/funcargs.py b/py/test/funcargs.py index 3c20f8ea5..a4a1ca61b 100644 --- a/py/test/funcargs.py +++ b/py/test/funcargs.py @@ -24,7 +24,8 @@ def fillfuncargs(function): _notexists = object() class CallSpec: - def __init__(self, id, param): + def __init__(self, funcargs, id, param): + self.funcargs = funcargs self.id = id if param is not _notexists: self.param = param @@ -40,14 +41,15 @@ class Metafunc: self._calls = [] self._ids = py.builtin.set() - def addcall(self, id=None, param=_notexists): + def addcall(self, funcargs=None, id=None, param=_notexists): + assert funcargs is None or isinstance(funcargs, dict) if id is None: id = len(self._calls) id = str(id) if id in self._ids: raise ValueError("duplicate id %r" % id) self._ids.add(id) - self._calls.append(CallSpec(id, param)) + self._calls.append(CallSpec(funcargs, id, param)) class FunctionCollector(py.test.collect.Collector): def __init__(self, name, parent, calls): @@ -57,9 +59,10 @@ class FunctionCollector(py.test.collect.Collector): def collect(self): l = [] - for call in self.calls: - function = self.parent.Function(name="%s[%s]" %(self.name, call.id), - parent=self, requestparam=call.param, callobj=self.obj) + for callspec in self.calls: + name = "%s[%s]" %(self.name, callspec.id) + function = self.parent.Function(name=name, parent=self, + callspec=callspec, callobj=self.obj) l.append(function) return l diff --git a/py/test/pycollect.py b/py/test/pycollect.py index fff54fc22..957f15318 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -326,14 +326,16 @@ class Function(FunctionMixin, py.test.collect.Item): and executing a Python callable test object. """ def __init__(self, name, parent=None, config=None, args=(), - requestparam=_dummy, callobj=_dummy): + callspec=None, callobj=_dummy): super(Function, self).__init__(name, parent, config=config) self._finalizers = [] self._args = args - if not args: # yielded functions (deprecated) have positional args - self.funcargs = {} - if requestparam is not _dummy: - self._requestparam = requestparam + if args: + assert not callspec, "yielded functions (deprecated) cannot have funcargs" + else: + self.funcargs = callspec and callspec.funcargs or {} + if hasattr(callspec, "param"): + self._requestparam = callspec.param if callobj is not _dummy: self._obj = callobj diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py index 01c926eb6..d9a263ea6 100644 --- a/py/test/testing/test_funcargs.py +++ b/py/test/testing/test_funcargs.py @@ -177,6 +177,16 @@ class TestMetafunc: assert metafunc._calls[1].param == obj assert metafunc._calls[2].param == 1 + def test_addcall_funcargs(self): + def func(arg1): pass + metafunc = funcargs.Metafunc(func) + class obj: pass + metafunc.addcall(funcargs={"x": 2}) + metafunc.addcall(funcargs={"x": 3}) + assert len(metafunc._calls) == 2 + assert metafunc._calls[0].funcargs == {'x': 2} + assert metafunc._calls[1].funcargs == {'x': 3} + assert not hasattr(metafunc._calls[1], 'param') class TestGenfuncFunctional: def test_attributes(self, testdir): @@ -208,6 +218,28 @@ class TestGenfuncFunctional: "*2 passed in*", ]) + def test_addcall_with_funcargs_two(self, testdir): + testdir.makeconftest(""" + class ConftestPlugin: + def pytest_generate_tests(self, metafunc): + assert "arg1" in metafunc.funcargnames + metafunc.addcall(funcargs=dict(arg1=1, arg2=2)) + """) + p = testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall(funcargs=dict(arg1=1, arg2=1)) + + class TestClass: + def test_myfunc(self, arg1, arg2): + assert arg1 == arg2 + """) + result = testdir.runpytest("-v", p) + assert result.stdout.fnmatch_lines([ + "*test_myfunc*0*PASS*", + "*test_myfunc*1*FAIL*", + "*1 failed, 1 passed*" + ]) + def test_two_functions(self, testdir): p = testdir.makepyfile(""" def pytest_generate_tests(metafunc):