V1 of the resources API draft
This commit is contained in:
parent
8adac2878f
commit
7a90bed19b
|
@ -609,3 +609,29 @@ class Session(FSCollector):
|
||||||
for x in self.genitems(subnode):
|
for x in self.genitems(subnode):
|
||||||
yield x
|
yield x
|
||||||
node.ihook.pytest_collectreport(report=rep)
|
node.ihook.pytest_collectreport(report=rep)
|
||||||
|
|
||||||
|
def register_resource_factory(self, name, factoryfunc,
|
||||||
|
matchscope=None,
|
||||||
|
cachescope=None):
|
||||||
|
""" register a factory function for the given name.
|
||||||
|
|
||||||
|
:param name: the name which can be used to retrieve a value constructed
|
||||||
|
by the factory function later.
|
||||||
|
:param factoryfunc: a function accepting (name, reqnode) parameters
|
||||||
|
and returning a value.
|
||||||
|
:param matchscope: denotes visibility of the factory func.
|
||||||
|
Pass a particular Node instance if you want to
|
||||||
|
restrict factory function visilbility to its descendants.
|
||||||
|
Pass None if you want the factory func to be globally
|
||||||
|
availabile.
|
||||||
|
:param cachescope: denotes caching scope. If you pass a node instance
|
||||||
|
the value returned by getresource() will be reused
|
||||||
|
for all descendants of that node. Pass None (the default)
|
||||||
|
if you want no caching. Pass "session" if you want to
|
||||||
|
to cache on a per-session level.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
|
||||||
|
V2: Creating and working with parametrized test resources
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
# XXX collection versus setup-time
|
||||||
|
# XXX parametrize-relation?
|
||||||
|
|
||||||
|
pytest-2.3 provides generalized resource management allowing
|
||||||
|
to flexibly manage caching and parametrization across your test suite.
|
||||||
|
|
||||||
|
This is draft documentation, pending refinements and changes according
|
||||||
|
to feedback and to implementation or backward compatibility issues
|
||||||
|
(the new mechanism is supposed to allow fully backward compatible
|
||||||
|
operations for uses of the "funcarg" mechanism.
|
||||||
|
|
||||||
|
the new global pytest_runtest_init hook
|
||||||
|
------------------------------------------------------
|
||||||
|
|
||||||
|
Prior to 2.3, pytest offered a pytest_configure and a pytest_sessionstart
|
||||||
|
hook which was used often to setup global resources. This suffers from
|
||||||
|
several problems. First of all, in distributed testing the master would
|
||||||
|
also setup test resources that are never needed because it only co-ordinates
|
||||||
|
the test run activities of the slave processes. Secondly, in large test
|
||||||
|
suites resources are setup that might not be needed for the concrete test
|
||||||
|
run. The first issue is solved through the introduction of a specific
|
||||||
|
hook::
|
||||||
|
|
||||||
|
def pytest_runtest_init(session):
|
||||||
|
# called ahead of pytest_runtestloop() test execution
|
||||||
|
|
||||||
|
This hook will only be called in processes that actually run tests.
|
||||||
|
|
||||||
|
The second issue is solved through a new register/getresource API which
|
||||||
|
will only ever setup resources if they are needed. See the following
|
||||||
|
examples and sections on how this works.
|
||||||
|
|
||||||
|
|
||||||
|
managing a global database resource
|
||||||
|
---------------------------------------------------------------
|
||||||
|
|
||||||
|
If you have one database object which you want to use in tests
|
||||||
|
you can write the following into a conftest.py file::
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self):
|
||||||
|
print ("database instance created")
|
||||||
|
def destroy(self):
|
||||||
|
print ("database instance destroyed")
|
||||||
|
|
||||||
|
def factory_db(name, node):
|
||||||
|
db = Database()
|
||||||
|
node.addfinalizer(db.destroy)
|
||||||
|
return db
|
||||||
|
|
||||||
|
def pytest_runtest_init(session):
|
||||||
|
session.register_resource("db", factory_db, atnode=session)
|
||||||
|
|
||||||
|
You can then access the constructed resource in a test like this::
|
||||||
|
|
||||||
|
def test_something(db):
|
||||||
|
...
|
||||||
|
|
||||||
|
The "db" function argument will lead to a lookup of the respective
|
||||||
|
factory value and be passed to the function body. According to the
|
||||||
|
registration, the db object will be instantiated on a per-session basis
|
||||||
|
and thus reused across all test functions that require it.
|
||||||
|
|
||||||
|
instantiating a database resource per-module
|
||||||
|
---------------------------------------------------------------
|
||||||
|
|
||||||
|
If you want one database instance per test module you can restrict
|
||||||
|
caching by modifying the "atnode" parameter of the registration
|
||||||
|
call above::
|
||||||
|
|
||||||
|
def pytest_runtest_init(session):
|
||||||
|
session.register_resource("db", factory_db, atnode=pytest.Module)
|
||||||
|
|
||||||
|
Neither the tests nor the factory function will need to change.
|
||||||
|
This also means that you can decide the scoping of resources
|
||||||
|
at runtime - e.g. based on a command line option: for developer
|
||||||
|
settings you might want per-session and for Continous Integration
|
||||||
|
runs you might prefer per-module or even per-function scope like this::
|
||||||
|
|
||||||
|
def pytest_runtest_init(session):
|
||||||
|
session.register_resource_factory("db", factory_db,
|
||||||
|
atnode=pytest.Function)
|
||||||
|
|
||||||
|
parametrized resources
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
If you want to rerun tests with different resource values you can specify
|
||||||
|
a list of factories instead of just one::
|
||||||
|
|
||||||
|
def pytest_runtest_init(session):
|
||||||
|
session.register_factory("db", [factory1, factory2], atnode=session)
|
||||||
|
|
||||||
|
In this case all tests that depend on the "db" resource will be run twice
|
||||||
|
using the respective values obtained from the two factory functions.
|
||||||
|
|
||||||
|
|
||||||
|
Using a resource from another resource factory
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
You can use the database resource from a another resource factory through
|
||||||
|
the ``node.getresource()`` method. Let's add a resource factory for
|
||||||
|
a "db_users" table at module-level, extending the previous db-example::
|
||||||
|
|
||||||
|
def pytest_runtest_init(session):
|
||||||
|
...
|
||||||
|
session.register_factory("db_users", createusers, atnode=module)
|
||||||
|
|
||||||
|
def createusers(name, node):
|
||||||
|
db = node.getresource("db")
|
||||||
|
table = db.create_table("users", ...)
|
||||||
|
node.addfinalizer(lambda: db.destroy_table("users")
|
||||||
|
|
||||||
|
def test_user_creation(db_users):
|
||||||
|
...
|
||||||
|
|
||||||
|
The create-users will be called for each module. After the tests in
|
||||||
|
that module finish execution, the table will be destroyed according
|
||||||
|
to registered finalizer. Note that calling getresource() for a resource
|
||||||
|
which has a tighter scope will raise a LookupError because the
|
||||||
|
is not available at a more general scope. Concretely, if you
|
||||||
|
table is defined as a per-session resource and the database object as a
|
||||||
|
per-module one, the table creation cannot work on a per-session basis.
|
||||||
|
|
||||||
|
|
||||||
|
Setting resources as class attributes
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
If you want to make an attribute available on a test class, you can
|
||||||
|
use the resource_attr marker::
|
||||||
|
|
||||||
|
@pytest.mark.resource_attr("db")
|
||||||
|
class TestClass:
|
||||||
|
def test_something(self):
|
||||||
|
#use self.db
|
||||||
|
|
||||||
|
Note that this way of using resources can be used on unittest.TestCase
|
||||||
|
instances as well (function arguments can not be added due to unittest
|
||||||
|
limitations).
|
||||||
|
|
||||||
|
|
||||||
|
How the funcarg mechanism is implemented (internal notes)
|
||||||
|
-------------------------------------------------------------
|
||||||
|
|
||||||
|
Prior to pytest-2.3/4, pytest advertised the "funcarg" mechanism
|
||||||
|
which provided a subset functionality to the generalized resource management.
|
||||||
|
In fact, the previous mechanism is implemented in terms of the new API
|
||||||
|
and should continue to work unmodified. It basically automates the
|
||||||
|
registration of factories through automatic discovery of
|
||||||
|
``pytest_funcarg_NAME`` function on plugins, Python modules and classes.
|
||||||
|
|
||||||
|
As an example let's consider the Module.setup() method::
|
||||||
|
|
||||||
|
class Module(PyCollector):
|
||||||
|
def setup(self):
|
||||||
|
for name, func in self.obj.__dict__.items():
|
||||||
|
if name.startswith("pytest_funcarg__"):
|
||||||
|
resourcename = name[len("pytest_funcarg__"):]
|
||||||
|
self._register_factory(resourcename,
|
||||||
|
RequestAdapter(self, name, func))
|
||||||
|
|
||||||
|
The request adapater takes care to provide the pre-2.3 API for funcarg
|
||||||
|
factories, providing request.cached_setup/addfinalizer/getfuncargvalue
|
||||||
|
methods.
|
Loading…
Reference in New Issue