Skip to main content

A fixture lifecycle management library for your tests

Project description

Build Status

Why do we need this library

We are not satisfied with classical xUnit way of setup and teardown. We prefer concise approach of py.test over the verbosity of standard unittest.

We found ourselves copying and pasting the same boilerplate code from one test to another or creating extensive structure of test class hierarchy.

py.test fixtures, injected in test functions as parameter names, is different approach for fixture management. It’s neither worse nor better, but we found it to be not as flexible as we need.

Some questions, that we wanted to solve often, looked like:

  • I have a py.test fixture which creates a new user with default set of properties. Is there a way I can create a user with different name by the same fixture?

  • Is there a way to create two users in one test case with the same fixture?

  • Is there an easy recipe to create a user first, and then, say, a todo item for this particular user in another, separate, fixture?

Sure enough, we can handle or work around all these issues somehow with xUnit setups and teardowns or py.test fixtures, but we wanted something more flexible, easy and convenient to use. That’s why we created resources library.

How do we use it

First, we define functions which we call “resource makers”. These makers are responsible for creating and destroying resources. It’s like setup and teardown in one callable.

from resources import resources

@resources.register_func
def user(email='joe@example.com', password='password', username='Joe'):
    user = User.objects.create(email=email, password=password, username=username)
    try:
        yield user
    finally:
        user.delete()

The flow is simple: we create, we yield, we destroy.

We get a number of resource makers, and we group them into modules, like tests/resources_core.py, tests/resources_users.py, etc.

Then, in a test file, where we plan to use resources, we import the same global object, load resource modules we need, and activate them in tests.

from resources import resources
resources.register_mod('tests.resources_core')
resources.register_mod('tests.resources_users')

def test_user_properties():
    with resources.user_ctx() as user:
        assert user.username == 'Joe'

This is where a little bit of magic happens. Once you define and register the resource maker with name foo, a context manager foo_ctx is created for your convenience. This context manager creates a new resource instance with the corresponding maker function, and destroys the object the way you defined, once the code flow abandons a wrapping “with”-context.

When it shines

At this point and maybe not so exciting. Yeah, everyone can write the code like this, the difference is that we actually did it :-). We also have a bunch of nifty features making the whole stuff more interesting.

Feature 1. Customizeable resources

Contexts are better than py.test fixtures, because they are customizeable. Provide everything you need to context manager, and it will be passed to resource maker function as an arguments.

def test_user_properties():
    with resources.user_ctx(name='Mary') as user:
        assert user.username == 'Mary'

Feature 2. Global object scope and dependent resources

We need to have access to resources at different stages of our tests: to get access to object’s properties and methods, to initiate another, dependent fixture instance, and finally to tear down everything.

As soon as you enter the context with resources.foo_ctx() a variable resources.foo will be created and will be available from everywhere, including your test function, and other resource makers.

The latter fact is especially important, because it’s the way we manage dependent resources. Yet we need some conventions, which resource is created first, and so on.

@resources.register_func
def todo_item(content='Foo'):
    item = TodoItem.objects.create(user=resources.user, content=content)

We agreed that we create user resource first, and todo item afterwards, and created a new resource maker, taking advantage of this convention.

We use it like this:

def test_todo_item_properties():
    with resources.user_ctx(), resources.todo_item_ctx():
        assert resources.todo_item.content == 'Foo'

By the way, if you are still stuck with python2.6, several context managers in the same “with” expression aren’t available for you yet. Use contextlib.nested to avoid deep indentation.

Feature 3. Several resources of the same class, and tuneable resource names

Sometimes we need to create a couple of resources of the same type, instead of just one instance. It’s not a problem, if you don’t want to use global namespace to get access to them. Otherwise you must create a unique identifier for every resource.

Actually, it’s trivial. All you should do is provide a special _name attribute to context manager constructor. This attribute won’t be passed to your resource maker function.

def test_a_couple_of_users():
    with resources.user_ctx(username='Adam', _name='adam'), \
         resurces.user_ctx(username='Eve', _name='eve'):
        assert resources.adam.username == 'Adam'
        assert resources.eve.username == 'Eve'

Feature 4. Function decorators

Context manager can work as a decorator too. When we use it like this, an extra argument will be passed to the function.

@resources.user_ctx()
def test_user_properties(user):
    assert user.username == 'Joe'

We should say that usually it works, but to make it work along with py.test which performs deep introspection of function signatures, we made in with some “dirty hacks” inside, and you may find out that in some cases the chain of decorators dies with a misleading exception. We’d recommend to use context managers instead of decorators, wherever possible.

Feature 5. Resource managers

Yes, we do use setup and teardown methods too. If every function in your test suite uses the same set of resources, it would be counterproductive to write the same chain of decorators or context managers over and over again.

In this case we use another concept: resource managers. Every resource maker foo creates the resources.foo_mgr instance, having start and stop methods. The start method accepts all arguments which the foo_ctx function does, including special _name argument. The stop method has only one optional _name argument, and is used to destroy previously created instance.

Here is a py.test example

def setup_function(func):
    resources.user_mgr.start(username='Mary')

def test_user_properties():
    assert resources.user.username == 'Mary'

def teardown_function(func):
    resources.user_mgr.stop()

Feature 6. Globally accessible storage of constants

This feature is not something unique to resources module. Pretty much every object can act this way, but it is handy to have a convention about the way you store your test-related constants.

It may work like this.

resources.TEST_DIRECTORY = '/tmp/foo'
resources.DOMAIN_NAME = 'example.com'
resources.SECRET_KEY = 'foobar'

And then, in the test file.

from resources import resources
resoures.register_mod('<a resource module name here>')

def test_constants():
    assert resources.TEST_DIRECTORY == '/tmp/foo'
    assert resources.DOMAIN_NAME == 'example.com'
    assert resources.SECRET_KEY == 'foobar'

Conclusion

The resources library works for us in py.test environment. We don’t see any reasons why it shouldn’t work the same way with nose or classic unitttests. It works for python versions 2.6, 2.7 and 3.3.

Please bear in mind that the library is not thread safe, as we are happy with single threaded tests at this time.

And after all… Six extra features to improve your test suites for free! What are you waiting for? It’s already improved the quailty of our lives in Doist Inc, and we do hope it will do the same for your projects.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python-resources-0.2.tar.gz (7.4 kB view details)

Uploaded Source

File details

Details for the file python-resources-0.2.tar.gz.

File metadata

File hashes

Hashes for python-resources-0.2.tar.gz
Algorithm Hash digest
SHA256 0cd4918056892f8dd4d44f46cd4c208e44b47e3fee8a4b37f296cbf79cc7d684
MD5 daedcf32bb8e69e42273ba588fd46a37
BLAKE2b-256 29052fed01d2c0c62f2d8a55b8a92aae6eea4c02e0d5bec929286875d708b33b

See more details on using hashes here.

Provenance

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page