I’ve been using pytest for writing tests because I absolutely love the simple assert systems. Although I’d love to write a small article detailing an easy introduction to pytest, I’d be digressing from the original title.
What exactly is the problem I’ll be describing: using pytest to share the same instance of setup and teardown code among multiple tests.
So instead of
setup --> test_1 --> teardown setup --> test_2 --> teardown
setup --> test_1 --> test_2 --> teardown
While testing, there is a need to create an environment for the test like a database, or an empty directory.
# I'm taking an example of a flask test application # In pytest we do this using advanced fixtures @pytest.fixtures(scope='function') def client(): # create a database # at the start of the test db.create_all() app_client = app.test_client() app_client.db = db yield app_client # remove the database # at the end of the test db.drop_all()
Most of the times we can make-do with a completely fresh environment for every test function we write, but at some point you’ll come across for a need to group multiple tests with the same instance of an environment.
Example, let’s say we want the user to register, login, and check his details all within the same database instance:
def test_user_register(client): ... def test_user_login(client): ... def test_user_details(client): ...
Pytest provides “scopes” for such groupings. Here pytest will
client fixture for every function separately, since
it’s scoped as a function. That means our database will lose
the changes made by the register function when it runs the
login function. To avoid losing these changes, we effectively
want to share the same fixture instance across these tests.
There is an easy way to solve this, we can just extract all
three tests into it’s own module and create a module scoped
should work as expected. But what if we need to do this across
several tests which belong to the several user stories?
Pytest documentation mostly highlights three scope levels namely: function, module, and session scopes, but there is another scope available, which is the class scope.
In a class level scope, we directly inject our environment into the class as instance variables, which are then shared among all the methods of the class.
First we create a class scoped fixture:
@pytest.fixture(scope='class') def class_client(request): # setup code db = ... db.create_all() # inject class variables request.cls.db = db yield # teardown code db.drop_all()
Here, as you can see we’re adding the database object, which needs to be shared among all the class methods, into the class of the current function which is run.
request.cls.db = db
This is done using the
request parameter provided by pytest to
give the request of the current test function.
To use this fixture on a class, we simply deocarate the class as follows:
@pytest.mark.usefixtures('class_client') class TestUserStory: def test_user_register(self): self.db.add(...) def test_user_login(self): # changes made in register are persisted # in the database object self.db.query(...) def test_user_details(self): ...
Now every function in the class can automatically access
the injected variables from the fixture as
That’s about it. I hope the explanation was clear and concise, any sort of feedback is appreciated! Thanks.