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,
- to get built-in
request
fixture inside out factory fixture and - 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.