Question

I'm trying to build a nice base around py.test

Some of our tests needs certain test data to work.

Today we just specify a mock object as a function argument and do the setup in the generator, this is clearly not desirable.

Here is an example of how it could look today:

def test_something(self, some_data):
    # some_data is unused in the test

I'd like to do something like this:

@uses_some_data
def test_something(self):
    # The data is loaded when the test is run

Though I have not figured out how to do this properly.

I cannot use class setup because I want the data to be persistant over the entire session, not setup/torn down on every test class.

My first idea was to still use funcargs but instead of letting the test have the funcarg we let the decorator request the funcarg for the function, basically hiding the ugliness.

The problem with this is that I need a py.test object to request a funcarg.
Is there any way I can get such an object or is this the wrong approach all together?

It would be an awesome bonus if the data did not have to be loaded if none of the collected tests requires the data, this is the downside of using decorators seeing as they are always run no matter if the test will be run or not.

Was it helpful?

Solution 2

After playing around some I found that this works:

def pytest_funcarg__some_data(request):
    def create():
        # Load the test data here
        print 'Test data loaded'

    return request.cached_setup(
        setup=create,
        scope='session',
        extrakey='some_data'
    )

def uses_some_data(func):
    # The funcarg is actually requested here
    def wrapper(self, some_data):
        return func
    return wrapper

class TestSomething(object):
    @uses_some_data
    def test_something(self):
        # "Some data" is now available
        pass

OTHER TIPS

Here is something that may work as-is, and if not will hopefully point you in the right direction.

class TestData(object):
    def __getattr__(self, name):
        if name not in ('data1', 'data2', 'data3'):
            raise AttributeError("TestData has no %s" % name)
        if name == 'data1':
            result = self._generate_data('data1')
            setattr(self.__class__, name, result)
        elif name == 'data2':
            result = self._generate_data('data2')
            setattr(self.__class__, name, result)
        elif name == 'data3':
            result = self._generate_data('data3')
            setattr(self.__class__, name, result)
        return result
    def _generate_data(self, data_name):
        return data_name * int(data_name[-1])

The TestData class uses the __getattr__ method to generate the data as it is needed, and by saving the generated date back to the class (not the instance!), the data is kept around for future use as well.

class uses_some_data(object):
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        global test_data
        test_data = TestData()
        return self.func(*args, **kwargs)

A simple decorator to set the global name binding for test_data. In fact, this version of the decorator is so simple it can be easily replaced with a module level name binding of test_data = TestData().

@uses_some_data
def testing_test():
    print(test_data.data2)

And a test function.

If you don't like the global level of test_data you could get fancier with the decorator and assign test_data to the function itself:

class uses_some_data(object):
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        self.func.test_data = TestData()
        return self.func(*args, **kwargs)

In this case, make sure your testing functions reference themselves

@uses_some_data
def testing_test():
    print(testing_test.test_data.data2)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top