I'm working on a rapidly growing Python project. Lately, our test suite started being somewhat unmanageable. Some tests fail when the modules they are in get executed in the wrong order, despite being seemingly well-isolated.
I found some other questions about this, but they were concerned with fixtures:
Pytest fixtures interfering with each other
test isolation between pytest-hypothesis runs
While we're using fixtures as well, I do not think the problem lies within them, but more likely in the fact that we use libraries where classes have internal state which gets changed by the test run, for example by mockito-python
.
I originally come from Java world where this does not happen unless you explicitly make your tests depend on each other or do some crazy and unusual things. Following the same set of practices in Python led me to these problems, so I realize I might be missing some crucial rule of Python test development.
Is it possible to tell pytest
to drop/revert/reinitialize the internal state of all classes between various module runs?
If not, which rules should we follow during test development to prevent these issues?
Edit: One of the issues we identified was that we had some objects set up at top level in the test file, which were created by mockito-python
. When the tests were executed, the files are first all imported and then the tests are executed. What happened was that one test was calling mockito.unstub()
which tears down all mocks in mockito's mock registry. So when the test was actually executed, another test had already torn down its mocks. This behavior is quite counter-intuitive, especially for unexperienced developers.
This plugin makes it possible to run tests quickly using multiprocessing (parallelism) and multithreading (concurrency).
pytest-ordering is a pytest plugin to run your tests in any order that you specify. It provides custom markers that say when your tests should run in relation to each other. They can be absolute (i.e. first, or second-to-last) or relative (i.e. run this test before this other test).
Project StructureThe modules containing pytests should be named “test_*. py” or “*_test.py”. While the pytest discovery mechanism can find tests anywhere, pytests must be placed into separate directories from the product code packages. These directories may either be under the project root or under the Python package.
One of the things that makes pytest's fixture system so powerful, is that it gives us the abilty to define a generic setup step that can reused over and over, just like a normal function would be used. Two different tests can request the same fixture and have pytest give each test their own result from that fixture.
In python it can happen easily that some state is modified by mistake. For example, when assigning a list to a variable, it is easy enough to forget to add a [:]
when it is desired that a copy of the list is created.
x = [0,1,2,3,4,5]
y = x # oops, should have been x[:]
y[2] = 7 # now we modify state somewhere...
x
=> [0, 1, 7, 3, 4, 5]
One possible approach to at least more likely identify such problems could be to execute your unit-tests in random order. I ran an experiment building upon the idea from https://stackoverflow.com/a/4006044/5747415:
import unittest
import random
def randcmp(_, x, y):
return random.randrange(-1, 2)
unittest.TestLoader.sortTestMethodsUsing = randcmp
As a result, the execution order of tests changed between the test executions. If by mistake your tests happen to have dependencies, you might be able to figure it out this way, because certain execution orders would lead to failures. Certainly, you would start on a small scale (only executing a small amount of tests), so you have the chance to more easily find the culprit.
Maybe worth trying...
Is it possible to tell pytest to drop/revert/reinitialize the internal state of all classes between various module runs?
You can achieve this by using importlib
to both invalidate cached modules and reload imported modules (thus refreshing the code & state).
This can be useful when code is required to run at the module level, for example in Flask where it's common to initialize app
at the module level in main.py
:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
In this case app
will be created whenever you import from main import ...
. You may want to test initializing app
in different ways, so in your test you could do something like
def test_app():
import main
importlib.reload(main) # reset module state
# do something with main.app
This would reset the state of the main
module during your test.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With