From 7a90bed19b836bd0fc29ece72a443b69f7cc8dbc Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 16 Jul 2012 10:47:00 +0200 Subject: [PATCH] V1 of the resources API draft --- _pytest/main.py | 26 ++++++ doc/en/example/resources.txt | 167 +++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 doc/en/example/resources.txt diff --git a/_pytest/main.py b/_pytest/main.py index abc4c1a5e..6ecb12dbd 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -609,3 +609,29 @@ class Session(FSCollector): for x in self.genitems(subnode): yield x 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. + """ + + + + + diff --git a/doc/en/example/resources.txt b/doc/en/example/resources.txt new file mode 100644 index 000000000..f37bfcda6 --- /dev/null +++ b/doc/en/example/resources.txt @@ -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.