pytest2/doc/en/resources.txt

427 lines
18 KiB
Plaintext

V3: Creating and working with parametrized test resources
===============================================================
**Target audience**: Reading this document requires basic knowledge of
python testing, xUnit setup methods and the basic pytest funcarg mechanism,
see http://pytest.org/latest/funcargs.html
**Abstract**: pytest-2.X provides more powerful and more flexible funcarg
and setup machinery. It does so by introducing a new @funcarg and a
new @setup marker which allows to define scoping and parametrization
parameters. If using ``@funcarg``, following the ``pytest_funcarg__``
naming pattern becomes optional. Functions decorated with ``@setup``
are called independenlty from the definition of funcargs but can
access funcarg values if needed. This allows for ultimate flexibility
in designing your test fixtures and their parametrization. Also,
you can now use ``py.test --collectonly`` to inspect your fixture
setup. Nonwithstanding these extensions, pre-existing test suites
and plugins written to work for previous pytest versions shall run unmodified.
**Changes**: This V3 draft is based on incorporating and thinking about
feedback provided by Floris Bruynooghe, Carl Meyer and Samuele Pedroni.
It remains as draft documentation, pending further refinements and
changes according to implementation or backward compatibility issues.
The main changes to V2 are:
* Collapse funcarg factory decorator into a single "@funcarg" one.
You can specify scopes and params with it. Moreover, if you supply
a "name" you do not need to follow the "pytest_funcarg__NAME" naming
pattern. Keeping with "funcarg" naming arguable now makes more
sense since the main interface using these resources are test and
setup functions. Keeping it probably causes the least semantic friction.
* Drop setup_directory/setup_session and introduce a new @setup
decorator similar to the @funcarg one but accepting funcargs.
* cosnider the extended setup_X funcargs for dropping because
the new @setup decorator probably is more flexible and introduces
less implementation complexity.
.. currentmodule:: _pytest
Shortcomings of the previous pytest_funcarg__ mechanism
---------------------------------------------------------
The previous funcarg mechanism calls a factory each time a
funcarg for a test function is requested. If a factory wants
t re-use a resource across different scopes, it often used
the ``request.cached_setup()`` helper to manage caching of
resources. Here is a basic example how we could implement
a per-session Database object::
# content of conftest.py
class Database:
def __init__(self):
print ("database instance created")
def destroy(self):
print ("database instance destroyed")
def pytest_funcarg__db(request):
return request.cached_setup(setup=DataBase,
teardown=lambda db: db.destroy,
scope="session")
There are some problems with this approach:
1. Scoping resource creation is not straight forward, instead one must
understand the intricate cached_setup() method mechanics.
2. parametrizing the "db" resource is not straight forward:
you need to apply a "parametrize" decorator or implement a
:py:func:`~hookspec.pytest_generate_tests` hook
calling :py:func:`~python.Metafunc.parametrize` which
performs parametrization at the places where the resource
is used. Moreover, you need to modify the factory to use an
``extrakey`` parameter containing ``request.param`` to the
:py:func:`~python.Request.cached_setup` call.
3. the current implementation is inefficient: it performs factory discovery
each time a "db" argument is required. This discovery wrongly happens at
setup-time.
4. there is no way how you can use funcarg factories, let alone
parametrization, when your tests use the xUnit setup_X approach.
5. there is no way to specify a per-directory scope for caching.
In the following sections, API extensions are presented to solve
each of these problems.
Direct scoping of funcarg factories
--------------------------------------------------------
Instead of calling cached_setup(), you can decorate your factory
to state its scope::
@pytest.mark.funcarg(scope="session")
def pytest_funcarg__db(request):
# factory will only be invoked once per session -
db = DataBase()
request.addfinalizer(db.destroy) # destroy when session is finished
return db
This factory implementation does not need to call ``cached_setup()`` anymore
because it will only be invoked once per session. Moreover, the
``request.addfinalizer()`` registers a finalizer according to the specified
resource scope on which the factory function is operating. With this new
scoping, the still existing ``cached_setup()`` should be much less used
but will remain for compatibility reasons and for the case where you
still want to have your factory get called on a per-item basis.
Direct parametrization of funcarg resource factories
----------------------------------------------------------
Previously, funcarg factories could not directly cause parametrization.
You needed to specify a ``@parametrize`` or implement a ``pytest_generate_tests`` hook to perform parametrization, i.e. calling a test multiple times
with different value sets. pytest-2.X introduces a decorator for use
on the factory itself::
@pytest.mark.funcarg(params=["mysql", "pg"])
def pytest_funcarg__db(request):
...
Here the factory will be invoked twice (with the respective "mysql"
and "pg" values set as ``request.param`` attributes) and and all of
the tests requiring "db" will run twice as well. The "mysql" and
"pg" values will also be used for reporting the test-invocation variants.
This new way of parametrizing funcarg factories should in many cases
allow to re-use already written factories because effectively
``request.param`` are already the parametrization attribute for test
functions/classes were parametrized via
:py:func:`~_pytest.python.Metafunc.parametrize(indirect=True)` calls.
Of course it's perfectly fine to combine parametrization and scoping::
@pytest.mark.funcarg(scope="session", params=["mysql", "pg"])
def pytest_funcarg__db(request):
if request.param == "mysql":
db = MySQL()
elif request.param == "pg":
db = PG()
request.addfinalizer(db.destroy) # destroy when session is finished
return db
This would execute all tests requiring the per-session "db" resource twice,
receiving the values created by the two respective invocations to the
factory function.
Direct usage of funcargs with funcargs factories
----------------------------------------------------------
You can now directly use funcargs in funcarg factories. Example::
@pytest.mark.funcarg(scope="session")
def db(request, tmpdir):
# tmpdir is a session-specific tempdir
Apart from convenience it also solves an issue when your factory
depends on a parametrized funcarg. Previously, a call to
``request.getfuncargvalue()`` would not allow pytest to know
at collection time about the fact that a required resource is
actually parametrized.
The "pytest_funcarg__" prefix becomes optional
-----------------------------------------------------
When using the ``@funcarg`` decorator you do not need to use
the ``pytest_funcarg__`` prefix any more::
@pytest.mark.funcarg
def db(request):
...
The name under which the funcarg resource can be requested is ``db``.
Any ``pytest_funcarg__`` prefix will be stripped. Note that a an
unqualified funcarg-marker implies a scope of "function" meaning
that the funcarg factory will be called for each test function invocation.
support for a new @setup marker
------------------------------------------------------
pytest for a long time offered a pytest_configure and a pytest_sessionstart
hook which are often used to setup global resources. This suffers from
several problems:
1. in distributed testing the master process would setup test resources
that are never needed because it only co-ordinates the test run
activities of the slave processes.
2. if you only perform a collection (with "--collectonly")
resource-setup will still be executed.
3. If a pytest_sessionstart is contained in some subdirectories
conftest.py file, it will not be called. This stems from the
fact that this hook is actually used for reporting, in particular
the test-header with platform/custom information.
4. there is no direct way how you can restrict setup to a directory scope.
Moreover, it is today not easy to define scoped setup from plugins or
conftest files other than to implement a ``pytest_runtest_setup()`` hook
and caring for scoping/caching yourself. And it's virtually impossible
to do this with parametrization as ``pytest_runtest_setup()`` is called
during test execution and parametrization happens at collection time.
It follows that pytest_configure/session/runtest_setup are often not
appropriate for implementing common fixture needs. Therefore,
pytest-2.X introduces a new "@pytest.mark.setup" marker, accepting
the same parameters as the @funcargs decorator. The difference is
that the decorated function can accept function arguments itself
Example::
# content of conftest.py
import pytest
@pytest.mark.setup(scope="session")
def mysetup(db):
...
This ``mysetup`` function is going to be executed when the first
test in the directory tree executes. It is going to be executed once
per-session and it receives the ``db`` funcarg which must be of same
of higher scope; you e. g. generally cannot use a per-module or per-function
scoped resource in a session-scoped setup function.
You can also use ``@setup`` inside a test module or class::
# content of test_module.py
import pytest
@pytest.mark.setup(scope="module", params=[1,2,3])
def modes(tmpdir, request):
# ...
This would execute the ``modes`` function once for each parameter.
In addition to normal funcargs you can also receive the "request"
funcarg which represents a takes on each of the values in the
``params=[1,2,3]`` decorator argument.
.. note::
For each scope, the funcargs will be setup and then the setup functions
will be called. This allows @setup-decorated functions to depend
on already setup funcarg values by accessing ``request.funcargs``.
Using funcarg resources in xUnit setup methods
------------------------------------------------------------
XXX Consider this feature in contrast to the @setup feature - probably
introducing one of them is better and the @setup decorator is more flexible.
For a long time, pytest has recommended the usage of funcarg
factories as a primary means for managing resources in your test run.
It is a better approach than the jUnit-based approach in many cases, even
more with the new pytest-2.X features, because the funcarg resource factory
provides a single place to determine scoping and parametrization. Your tests
do not need to encode setup/teardown details in every test file's
setup_module/class/method.
However, the jUnit methods originally introduced by pytest to Python,
remain popoular with nose and unittest-based test suites. Without question,
there are large existing test suites using this paradigm. pytest-2.X
recognizes this fact and now offers direct integration with funcarg resources. Here is a basic example for getting a per-module tmpdir::
def setup_module(mod, tmpdir):
mod.tmpdir = tmpdir
This will trigger pytest's funcarg mechanism to create a value of
"tmpdir" which can then be used throughout the module as a global.
The new extension to setup_X methods also works in case a resource is
parametrized. For example, let's consider an setup_class example using
our "db" resource::
class TestClass:
def setup_class(cls, db):
cls.db = db
# perform some extra things on db
# so that test methods can work with it
With pytest-2.X the setup* methods will be discovered at collection-time,
allowing to seemlessly integrate this approach with parametrization,
allowing the factory specification to determine all details. The
setup_class itself does not itself need to be aware of the fact that
"db" might be a mysql/PG database.
Note that if the specified resource is provided only as a per-testfunction
resource, collection would early on report a ScopingMismatch error.
the "directory" caching scope
--------------------------------------------
All API accepting a scope (:py:func:`cached_setup()` and
the new funcarg/setup decorators) now also accept a "directory"
specification. This allows to restrict/cache resource values on a
per-directory level.
funcarg and setup discovery now happens at collection time
---------------------------------------------------------------------
pytest-2.X takes care to discover funcarg factories and setup_X methods
at collection time. This is more efficient especially for large test suites.
Moreover, a call to "py.test --collectonly" should be able to show
a lot of setup-information and thus presents a nice method to get an
overview of resource management in your project.
Implementation level
===================================================================
To implement the above new features, pytest-2.X grows some new hooks and
methods. At the time of writing V2 and without actually implementing
it, it is not clear how much of this new internal API will also be
exposed and advertised e. g. for plugin writers.
The main effort, however, will lie in revising what is done at
collection and what at test setup time. All funcarg factories and
xUnit setup methods need to be discovered at collection time
for the above mechanism to work. Additionally all test function
signatures need to be parsed in order to know which resources are
used. On the plus side, all previously collected fixtures and
test functions only need to be called, no discovery is neccessary
is required anymore.
the "request" object incorporates scope-specific behaviour
------------------------------------------------------------------
funcarg factories receive a request object to help with implementing
finalization and inspection of the requesting-context. If there is
no scoping is in effect, nothing much will change of the API behaviour.
However, with scoping the request object represents the according context.
Let's consider this example::
@pytest.mark.factory_scope("class")
def pytest_funcarg__db(request):
# ...
request.getfuncargvalue(...)
#
request.addfinalizer(db)
Due to the class-scope, the request object will:
- provide a ``None`` value for the ``request.function`` attribute.
- default to per-class finalization with the addfinalizer() call.
- raise a ScopeMismatchError if a more broadly scoped factory
wants to use a more tighly scoped factory (e.g. per-function)
In fact, the request object is likely going to provide a "node"
attribute, denoting the current collection node on which it internally
operates. (Prior to pytest-2.3 there already was an internal
_pyfuncitem).
As these are rather intuitive extensions, not much friction is expected
for test/plugin writers using the new scoping and parametrization mechanism.
It's, however, a serious internal effort to reorganize the pytest
implementation.
node.register_factory/getresource() methods
--------------------------------------------------------
In order to implement factory- and setup-method discovery at
collection time, a new node API will be introduced to allow
for factory registration and a getresource() call to obtain
created values. The exact details of this API remain subject
to experimentation. The basic idea is to introduce two new
methods to the Session class which is already available on all nodes
through the ``node.session`` attribute::
class Session:
def register_resource_factory(self, name, factory_or_list, scope):
""" register a resource factory for the given name.
:param name: Name of the resource.
:factory_or_list: a function or a list of functions creating
one or multiple resource values.
:param scope: a node instance. The factory will be only visisble
available for all descendant nodes.
specify the "session" instance for global availability
"""
def getresource(self, name, node):
""" get a named resource for the give node.
This method looks up a matching funcarg resource factory
and calls it.
"""
.. todo::
XXX While this new API (or some variant of it) may suffices to implement
all of the described new usage-level features, it remains unclear how the
existing "@parametrize" or "metafunc.parametrize()" calls will map to it.
These parametrize-approaches tie resource parametrization to the
function/funcargs-usage rather than to the factories.
ISSUES
--------------------------
decorating a parametrized funcarg factory:
@pytest.mark.funcarg(scope="session", params=["mysql", "pg"])
def db(request):
...
class TestClass:
@pytest.mark.funcarg(scope="function")
def something(self, request):
session_db = request.getfuncargvalue("db")
...
Here the function-scoped "something" factory uses the session-scoped
"db" factory to perform some additional steps. The dependency, however,
is only visible at setup-time, when the factory actually gets called.
In order to allow parametrization at collection-time I see two ways:
- allow specifying dependencies in the funcarg-marker
- allow funcargs for factories as well