Quick Note: Tearing Down a Factory Fixture in Pytest

Quick Note is going to be series where I take quick notes about how I have solved a particular problem with quick notes and code pieces.

As you know, a factory fixture in Pytest is a fixture which returns a function. An example would be:

@pytest.fixture
def read_test_resource():
    def factory(file_name, mode = "r"):
        file = open("tests/resources/{}".format(file_name), mode)
        return file

    return factory

def test_foo(read_test_resource):
    file = read_test_resource("foo.txt")

This example is cool and all, but there's a catch. It is usually (by usually, I mean almost always) a good practice to release a resource that you loaded from the disk. In other words, we need to call file.close() in the code above. However, since we return it is too late to release the resource. One thing is you can do it manually by hand in the tests somewhere, like in the test_foo above, after you have finished all the testing and stuff.

However, it would be great to automate this process, as in releasing the resource at the end of tests automatically, without writing file.close() in the tests. This also removes the factor that we might forget to call file.close().

What we need is, simply, a teardown operation. It would be simple in x-unit style testing. It is still simple with factory architecture. Pytest suggests one way and that is to use yield instead of return. And whatever comes after yield will be executed after the scope of the fixture, which is the function here.

However, what we use is not simply a fixture. It is a factory fixture, which returns a function. That is why it is not okay for us to call yield anywhere. We cannot do it in factory() because we already return from there. And file is not available in the outer scope as well. Let's check the code again with these in mind:

@pytest.fixture
def read_test_resource():
    def factory(file_name, mode = "r"):
        file = open("tests/resources/{}".format(file_name), mode)
        return file  # we cannot use yield here, it will complain about generator

    return factory  # we do not have access to file here

def test_foo(read_test_resource):
    file = read_test_resource("foo.txt")

So, there's a second method of pytest. It is, step by step,

  1. to get built-in request fixture inside out factory fixture and
  2. use request.addfinalize(finalizing_method) to say what happens after the fixture has been done

In code:

@pytest.fixture
def read_test_resource(request):  # pytest's built-in request fixture
    def factory(file_name, mode = "r"):
        file = open("tests/resources/{}".format(file_name), mode)
        request.addfinalizer(lambda: file.close())  # we simply add a anonymous function here
        return file

    return factory  # we do not have access to file here

def test_foo(read_test_resource):
    file = read_test_resource("foo.txt")

Now the file will be closed after fixture's scope is done.

Eray Erdin

Believing one day we will carry mobile oxygen tubes along and an umbrella which protects us from acidic rain under the cancer-distributing green sky.

Write your comment…

Be the first one to comment