addcall now takes direct funcargs values alternatively

--HG--
branch : trunk
This commit is contained in:
holger krekel 2009-05-13 03:01:02 +02:00
parent 65a04bc3be
commit 19c9506fa3
5 changed files with 88 additions and 54 deletions

View File

@ -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`:

View File

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

View File

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

View File

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

View File

@ -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):