Why cant unittest.TestCases see my py.test fixtures? - python

I'm trying to use py.test's fixtures with my unit tests, in conjunction with unittest. I've put several fixtures in a conftest.py file at the top level of the project (as described here), decorated them with #pytest.fixture, and put their names as arguments to the test functions that require them.
The fixtures are registering correctly, as shown by py.test --fixtures test_stuff.py, but when I run py.test, I get NameError: global name 'my_fixture' is not defined. This appears to only occur when I use subclasses of unittest.TestCase—but the py.test docs seem to say that it plays well with unittest.
Why can't the tests see the fixtures when I use unittest.TestCase?
Doesn't work:
conftest.py
#pytest.fixture
def my_fixture():
return 'This is some fixture data'
test_stuff.py
import unittest
import pytest
class TestWithFixtures(unittest.TestCase):
def test_with_a_fixture(self, my_fixture):
print(my_fixture)
Works:
conftest.py
#pytest.fixture()
def my_fixture():
return 'This is some fixture data'
test_stuff.py
import pytest
class TestWithFixtures:
def test_with_a_fixture(self, my_fixture):
print(my_fixture)
I'm asking this question more out of curiosity; for now I'm just ditching unittest altogether.

While pytest supports receiving fixtures via test function arguments
for non-unittest test methods, unittest.TestCase methods cannot
directly receive fixture function arguments as implementing that is
likely to inflict on the ability to run general unittest.TestCase test
suites.
From the note section at the bottom of:
https://pytest.org/en/latest/unittest.html
It's possible to use fixtures with unittest.TestCasees. See that page for more information.

You can use the pytest fixtures in unittest.TestCase with the pytest option autouse. However, if you use the test_ for the unit method using the fixture the following error will appear:
Fixtures are not meant to be called directly,...
### conftest.py
#pytest.fixture
def my_fixture():
return 'This is some fixture data'
One solution is to use a prepare_fixture method to set fixtures as an attribute of the TestWithFixtures class, so that fixtures are available to all unit test methods.
### test_stuff.py
import unittest
import pytest
class TestWithFixtures(unittest.TestCase):
#pytest.fixture(autouse=True)
def prepare_fixture(self, my_fixture):
self.myfixture = my_fixture
def test_with_a_fixture(self):
print(self.myfixture)

define the fixture as an accessible variable, (like input in the following example). To define it, use request.cls.VARIABLE_NAME_YOU_DEFINE = RETURN_VALUE
use #pytest.mark.usefixtures("YOUR_FIXTURE") to use fixture outside of the unittest class, inside the unittest class, access the fixture by self.VARIABLE_NAME_YOU_DEFINE.
e.g.
import unittest
import pytest
#pytest.fixture(scope="class")
def test_input(request):
request.cls.input = {"key": "value"}
#pytest.mark.usefixtures("test_input")
class MyTestCase(unittest.TestCase):
def test_something(self):
self.assertEqual(self.input["key"], "value")

Related

How are pytest fixure scopes intended to work?

I want to use pytest fixtures to prepare an object I want to use across a set of tests.
I follow the documentation and create a fixture in something_fixture.py with its scope set to session like this:
import pytest
#pytest.fixture(scope="session")
def something():
return 'something'
Then in test_something.py I try to use the fixture like this:
def test_something(something):
assert something == 'something'
Which does not work, but if I import the fixture like this:
from tests.something_fixture import something
def test_something(something):
assert something == 'something'
the test passes...
Is this import necessary? Because to me this is not clear according to the documentation.
This session-scoped fixture should be defined in a conftest.py module, see conftest.py: sharing fixtures across multiple files in the docs.
The conftest.py file serves as a means of providing fixtures for an entire directory. Fixtures defined in a conftest.py can be used by any test in that package without needing to import them (pytest will automatically discover them).
By writing the fixture in something_fixture.py it was defined somewhere that went "unnoticed" because there was no reason for Python to import this module. The default test collection phase considers filenames matching these glob patterns:
- test_*.py
- *_test.py
Since it's a session-scoped feature, define it instead in a conftest.py file, so it will be created at test collection time and available to all tests.
You can remove the import statement from tests.something_fixture import something. In fact the "tests" subdirectory generally doesn't need to be importable at all.

override autouse=True pytest fixture [duplicate]

Let's say I have a very simple logging decorator:
from functools import wraps
def my_decorator(func):
#wraps(func)
def wrapper(*args, **kwargs):
print(f"{func.__name__} ran with args: {args}, and kwargs: {kwargs}")
result = func(*args, **kwargs)
return result
return wrapper
I can add this decorator to every pytest unit test individually:
#my_decorator
def test_one():
assert True
#my_decorator
def test_two():
assert 1
How can I automatically add this decorator to every single pytest unit test so I don't have to add it manually? What if I want to add it to every unit test in a file? Or in a module?
My use case is to wrap every test function with a SQL profiler, so inefficient ORM code raises an error. Using a pytest fixture should work, but I have thousands of tests so it would be nice to apply the wrapper automatically instead of adding the fixture to every single test. Additionally, there may be a module or two I don't want to profile so being able to opt-in or opt-out an entire file or module would be helpful.
Provided you can move the logic into a fixture, as stated in the question, you can just use an auto-use fixture defined in the top-level conftest.py.
To add the possibility to opt out for some tests, you can define a marker that will be added to the tests that should not use the fixture, and check that marker in the fixture, e.g. something like this:
conftest.py
import pytest
def pytest_configure(config):
config.addinivalue_line(
"markers",
"no_profiling: mark test to not use sql profiling"
)
#pytest.fixture(autouse=True)
def sql_profiling(request):
if not request.node.get_closest_marker("no_profiling"):
# do the profiling
yield
test.py
import pytest
def test1():
pass # will use profiling
#pytest.mark.no_profiling
def test2():
pass # will not use profiling
As pointed out by #hoefling, you could also disable the fixture for a whole module by adding:
pytestmark = pytest.mark.no_profiling
in the module. That will add the marker to all contained tests.

py.test patch on fixture

I use the following to mock constant values for a test with py.test:
#patch('ConstantsModule.ConstantsClass.DELAY_TIME', 10)
def test_PowerUp():
...
thing = Thing.Thing()
assert thing.a == 1
This mocks DELAY_TIME as used in both the test, and in Thing, which is what I expected.
I wanted to do this for all the tests in this file, so I tried
#patch('ConstantsModule.ConstantsClass.DELAY_TIME', 10)
#pytest.fixture(autouse=True)
def NoDelay():
pass
But that doesn't seem to have the same effect.
Here is a similar question: pytest-mock mocker in pytest fixture, but the mock seems done in a non-decorator way there.
I'd say patching via decorator is not the optimal approach here. I'd use the context manager:
import pytest
from unittest.mock import patch
#pytest.fixture(autouse=True)
def no_delay():
with patch('ConstantsModule.ConstantsClass.DELAY_TIME', 10):
yield
This way, patching is cleanly reverted on test teardown.
pytest provides builtin patching support via the monkeypatch fixture. So to patch the constant for all tests in the file you can create the following autouse fixture:
#pytest.fixture(autouse=True)
def no_delay(monkeypatch):
monkeypatch.setattr(ConstantsModule.ConstantsClass, 'DELAY_TIME', 10)
If you don't want to have the ConstantsModule imported in your tests you can use a string, see the full API reference.

How to get django's unittest TestLoader to find and run my doctests?

In Django, my tests are a set of test_foo.py files inside my_django_app/tests/, which each contain a TestCase subclass, and which django automatically finds and runs.
I have a bunch of utility modules with simple doctests that I would like to be included with my test suite. I tried using doctest.DocTestSuite() to define test suites in my_django_app/tests/test_doctests.py, but django's test runner does not find the new tests in that module.
Is there a way I can create a TestCase class that calls my doctests, or somehow otherwise define a new tests/test_foo.py module that would run these tests?
The automagic of Django unittests discovery looks for a load_tests function in your test_foo module and will run it. So you can use that to add your doctests to the test suite ...
import doctest
import module_with_doctests
def load_tests(loader, tests, ignore):
tests.addTests(doctest.DocTestSuite(module_with_doctests))
return tests
Also, due to a bug(?) in unittest your load_tests function won't be run unless your test_foo module also defines a TestCase-derived class like so:
class DoNothingTest(TestCase):
"""Encourage Django unittests to run `load_tests()`."""
def test_example(self):
self.assertTrue(True)
I solved this by creating a new module, my_django_app/tests/test_doctests.py, that looks like:
import doctest
import unittest
# These are my modules that contain doctests:
from util import bitwise
from util import text
from util import urlutil
DOCTEST_MODULES = (
bitwise,
text,
urlutil,
)
# unittest.TestLoader will call this when it finds this module:
def load_tests(*args, **kwargs):
test_all_doctests = unittest.TestSuite()
for m in DOCTEST_MODULES:
test_all_doctests.addTest(doctest.DocTestSuite(m))
return test_all_doctests
Django uses the builtin unittest TestLoader, which, during test discovery, will call load_tests() on your test module. So we define load_tests which creates a test suite out of all of the doctests.
import django.test.runner
testsuite = django.test.runner.DiscoverRunner().build_suite()
but, as best I can tell, basic unittest discover produces the same collection
import unittest
testsuite = unittest.TestLoader().discover('.')
unrelated
I did notice that unittest and django.test appear to use an internal attribute TestCase._testMethodName differently, in unittest, this is the testcase module+class namespace, in django, this appeared to be a random testcase method name, with the module and class being attributes of self.__module__. probably try to avoid needing to poke around in internals anyway though

Can pytest fixtures be combined?

Can one fixture build on another in pytest? I have a very simple fixture called "cleaner" defined as:
import pytest
from mypackage import db
#pytest.fixture()
def cleaner(request):
def finalizer():
db.clear()
request.addfinalizer(finalizer)
then in my setup.cfg I have:
[pytest]
norecursedirs = .git venv
usefixtures = cleaner
This results in the database being truncated after each test, which is great. But now I want other fixtures I make to also call the finalizer from cleaner. Is there a way to define another fixture that somehow expands on or calls cleaner?
You have to declare that your other fixture depends on cleaner explicitly:
import pytest
#pytest.fixture
def cleaner(request):
def finalizer():
print '\n"cleaner" finalized'
print '\n"cleaner" fixture'
request.addfinalizer(finalizer)
#pytest.fixture
def other(cleaner):
print '\n"other" fixture'
def test_foo(other):
pass
Running this with py.test -s -v produces:
test_foo.py#16::test_foo
"cleaner" fixture
"other" fixture
PASSED
"cleaner" finalized

Categories