Pytest inheritance problems
While working on a project that was made from a core package and extension packages. The extension packages were meant as a way to quickly and easily both modify and add to the core functionality.
When extending, we also wanted to be able to leverage all the tests from the core package and not duplicate or rewrite the parts that were testing modified core functionality.
Trying to do this was a bit of a trial and error, so hopefully you are reading this before you spend too much time on the the same (easily fixable) problems as we did.
Let us look at an example that uses a core Humans
package, extended by a Presidents
package. The full code is available on GitHub.
The core 'Humans' package
Let's say we are provided with a Humans
core package that we wish to be able to extend later. This package includes:
- A class definition (in
human.py
) - Tests (in
conftest.py
andtest_human.py
)
The class definition is as follows:
class Human:
def baby_hello(self):
return "Googoo gaga gaga da gaagaa"
def hello(self):
return "Hello, I am a normal human!"
The fixtures defined in conftest.py
are:
import pytest
@pytest.fixture
def Human():
from humans.human import Human
return Human
@pytest.fixture
def human(Human):
return Human()
And the actual tests in test_human.py
look like:
class TestHuman:
def test_baby_hello(self, human):
assert human.baby_hello() == "Googoo gaga gaga da gaagaa"
def test_hello(self, human):
assert human.hello() == "Hello, I am a normal human!"
The output of running py.test
in this package is the expected:
$ py.test
====================================== test session starts ======================================
platform linux -- Python 3.4.2 -- py-1.4.26 -- pytest-2.6.4
collected 2 items
test_human.py ..
=================================== 2 passed in 0.04 seconds ====================================
The extension 'Presidents' package
This is our extension package which will use the Humans
package as a core for easier development. The structure mirrors the Humans
package (so we have president.py
, conftest.py
and test_president.py
).
First we create an extender class in president.py
:
from humans.human import Human
class President(Human):
def hello(self):
return "I am very important - a president!"
If you take the time and test it manually, you will see that everything seems to be ok and work fine. Of course since you are no doubt a good developer/programmer, you also want to test your code automatically and not only manually. Since we do not want to redo all tests from the Humans
package (and then have to keep them synchronised if they change) we want to leverage them and simply extend the tests in a similar way as we did with the class.
Let's start with the tests. Going by the same logic as before, extending the test class and overriding the necessary methods should work fine, which gives us a test_president.py
file with the following code:
from humans.test_human import TestHuman
class TestPresident(TestHuman):
def test_hello(self, human):
assert human.hello() == "I am very important - a president!"
Of course we also need to do something with the fixtures, or this will use a Human
class instead of a President
. We could override the human
fixture, but we can also do the following:
import pytest
from humans.tests.conftest import human
@pytest.fixture
def Human():
from mapp_app.gaza.models.president import President
return President
We have simply overridden the Human
fixture to return the President
class, so the human
fixture - using the overridden Human
fixture will return an instance of the President
class.
Keep in mind that we still need to import the human
fixture or our tests will not see the fixture and fail.
Let us run py.test
, expecting we will see two passing tests:
$ py.test
====================================== test session starts ======================================
platform linux -- Python 3.4.2 -- py-1.4.26 -- pytest-2.6.4
collected 4 items
test_president.py .F..
=========================================== FAILURES ============================================
_____________________________________ TestHuman.test_hello ______________________________________
self = <humans.test_human.TestHuman object at 0x7f14731b1a20>
human = <presidents.president.President object at 0x7f14731b11d0>
def test_hello(self, human):
> assert human.hello() == "Hello, I am a normal human!"
E assert 'I am very im... a president!' == 'Hello, I am a normal human!'
E - I am very important - a president!
E + Hello, I am a normal human!
../humans/test_human.py:6: AssertionError
============================== 1 failed, 3 passed in 0.07 seconds ===============================
What exactly happened here? pytest found four tests instead of just two, three of which passed and one failed ... let's get some more information:
$ py.test -v
====================================== test session starts ======================================
platform linux -- Python 3.4.2 -- py-1.4.26 -- pytest-2.6.4 -- /home/natan/projects/termitnjak/blog/pytest_inheritance/code/bin/python
collected 4 items
test_president.py <- ../humans/test_human.py::TestHuman::test_baby_hello PASSED
test_president.py <- ../humans/test_human.py::TestHuman::test_hello FAILED
test_president.py <- ../humans/test_human.py::TestPresident::test_baby_hello PASSED
test_president.py::TestPresident::test_hello PASSED
=========================================== FAILURES ============================================
_____________________________________ TestHuman.test_hello ______________________________________
self = <humans.test_human.TestHuman object at 0x7fd1f0c50828>
human = <presidents.president.President object at 0x7fd1f0c50a20>
def test_hello(self, human):
> assert human.hello() == "Hello, I am a normal human!"
E assert 'I am very im... a president!' == 'Hello, I am a normal human!'
E - I am very important - a president!
E + Hello, I am a normal human!
../humans/test_human.py:6: AssertionError
============================== 1 failed, 3 passed in 0.07 seconds ===============================
Now we can see what happened - in addition to our new extended test the old tests were still collected and run, but with the new, overridden fixtures. Not only does this fail, but it also causes us to re-run all old tests, which could take a long time in bigger packages.
Thankfully, there is a simple way to fix this - pytest support various nose idioms and __test__
is exactly what we are looking for. We simply need to set TestHuman.__test__
to False
and it's tests shouldn't be collected at all. Adding this fix leaves us with:
from humans.test_human import TestHuman
TestHuman.__test__ = False
class TestPresident(TestHuman):
__test__ = True
def test_hello(self, human):
assert human.hello() == "I am very important - a president!"
And running py.test -v
with the fix returns:
$ py.test -v
====================================== test session starts ======================================
platform linux -- Python 3.4.2 -- py-1.4.26 -- pytest-2.6.4 -- /home/natan/projects/termitnjak/blog/pytest_inheritance/code/bin/python
collected 2 items
test_president.py <- ../humans/test_human.py::TestPresident::test_baby_hello PASSED
test_president.py::TestPresident::test_hello PASSED
=================================== 2 passed in 0.06 seconds ====================================
Which is exactly what we wanted - use the test_baby_hello
test from the old package, but use our new overridden version of test_hello
.
Conclusion
Not that hard right? Just remember to set __test__
to False
for every testing class you import and make sure to override/setup the fixtures too.
Now take the time you saved by re-using tests to grab a cup of coffee or tea and leave a comment below.
Share this post
comments powered by Disqus