Skip to content

Commit 6f7fc26

Browse files
authored
Add optional integration of pytest into django's manage.py test
To lower the barrier of entry for questions like #1169.
1 parent 3071a66 commit 6f7fc26

File tree

5 files changed

+124
-46
lines changed

5 files changed

+124
-46
lines changed

docs/faq.rst

+3-44
Original file line numberDiff line numberDiff line change
@@ -79,53 +79,12 @@ How can I use ``manage.py test`` with pytest-django?
7979
----------------------------------------------------
8080

8181
pytest-django is designed to work with the ``pytest`` command, but if you
82-
really need integration with ``manage.py test``, you can create a simple
83-
test runner like this:
82+
really need integration with ``manage.py test``, you can add this class path
83+
in your Django settings:
8484

8585
.. code-block:: python
8686
87-
class PytestTestRunner:
88-
"""Runs pytest to discover and run tests."""
89-
90-
def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs):
91-
self.verbosity = verbosity
92-
self.failfast = failfast
93-
self.keepdb = keepdb
94-
95-
@classmethod
96-
def add_arguments(cls, parser):
97-
parser.add_argument(
98-
'--keepdb', action='store_true',
99-
help='Preserves the test DB between runs.'
100-
)
101-
102-
def run_tests(self, test_labels, **kwargs):
103-
"""Run pytest and return the exitcode.
104-
105-
It translates some of Django's test command option to pytest's.
106-
"""
107-
import pytest
108-
109-
argv = []
110-
if self.verbosity == 0:
111-
argv.append('--quiet')
112-
if self.verbosity == 2:
113-
argv.append('--verbose')
114-
if self.verbosity == 3:
115-
argv.append('-vv')
116-
if self.failfast:
117-
argv.append('--exitfirst')
118-
if self.keepdb:
119-
argv.append('--reuse-db')
120-
121-
argv.extend(test_labels)
122-
return pytest.main(argv)
123-
124-
Add the path to this class in your Django settings:
125-
126-
.. code-block:: python
127-
128-
TEST_RUNNER = 'my_project.runner.PytestTestRunner'
87+
TEST_RUNNER = 'pytest_django.runner.TestRunner'
12988
13089
Usage:
13190

pytest_django/runner.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from argparse import ArgumentParser
2+
from typing import Any, Iterable
3+
4+
5+
class TestRunner:
6+
"""A Django test runner which uses pytest to discover and run tests when using `manage.py test`."""
7+
8+
def __init__(
9+
self,
10+
*,
11+
verbosity: int = 1,
12+
failfast: bool = False,
13+
keepdb: bool = False,
14+
**kwargs: Any,
15+
) -> None:
16+
self.verbosity = verbosity
17+
self.failfast = failfast
18+
self.keepdb = keepdb
19+
20+
@classmethod
21+
def add_arguments(cls, parser: ArgumentParser) -> None:
22+
parser.add_argument(
23+
"--keepdb", action="store_true", help="Preserves the test DB between runs."
24+
)
25+
26+
def run_tests(self, test_labels: Iterable[str], **kwargs: Any) -> int:
27+
"""Run pytest and return the exitcode.
28+
29+
It translates some of Django's test command option to pytest's.
30+
"""
31+
import pytest
32+
33+
argv = []
34+
if self.verbosity == 0:
35+
argv.append("--quiet")
36+
elif self.verbosity >= 2:
37+
verbosity = "v" * (self.verbosity - 1)
38+
argv.append(f"-{verbosity}")
39+
if self.failfast:
40+
argv.append("--exitfirst")
41+
if self.keepdb:
42+
argv.append("--reuse-db")
43+
44+
argv.extend(test_labels)
45+
return pytest.main(argv)

tests/conftest.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,16 @@ def django_pytester(
117117
tpkg_path.mkdir()
118118

119119
if options["create_manage_py"]:
120-
project_root.joinpath("manage.py").touch()
120+
project_root.joinpath("manage.py").write_text(
121+
dedent(
122+
"""
123+
#!/usr/bin/env python
124+
import sys
125+
from django.core.management import execute_from_command_line
126+
execute_from_command_line(sys.argv)
127+
"""
128+
)
129+
)
121130

122131
tpkg_path.joinpath("__init__.py").touch()
123132

tests/test_environment.py

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23

34
import pytest
45
from django.contrib.sites import models as site_models
@@ -308,7 +309,6 @@ class TestrunnerVerbosity:
308309

309310
@pytest.fixture
310311
def pytester(self, django_pytester: DjangoPytester) -> pytest.Pytester:
311-
print("pytester")
312312
django_pytester.create_test_module(
313313
"""
314314
import pytest
@@ -379,3 +379,42 @@ def test_clear_site_cache_check_site_cache_size(site_name: str, settings) -> Non
379379
settings.SITE_ID = site.id
380380
assert Site.objects.get_current() == site
381381
assert len(site_models.SITE_CACHE) == 1
382+
383+
384+
@pytest.mark.django_project(
385+
project_root="django_project_root",
386+
create_manage_py=True,
387+
extra_settings="""
388+
TEST_RUNNER = 'pytest_django.runner.TestRunner'
389+
""",
390+
)
391+
def test_manage_test_runner(django_pytester: DjangoPytester) -> None:
392+
django_pytester.create_test_module(
393+
"""
394+
import pytest
395+
396+
@pytest.mark.django_db
397+
def test_inner_testrunner():
398+
pass
399+
"""
400+
)
401+
result = django_pytester.run(*[sys.executable, "django_project_root/manage.py", "test"])
402+
assert "1 passed" in "\n".join(result.outlines)
403+
404+
405+
@pytest.mark.django_project(
406+
project_root="django_project_root",
407+
create_manage_py=True,
408+
)
409+
def test_manage_test_runner_without(django_pytester: DjangoPytester) -> None:
410+
django_pytester.create_test_module(
411+
"""
412+
import pytest
413+
414+
@pytest.mark.django_db
415+
def test_inner_testrunner():
416+
pass
417+
"""
418+
)
419+
result = django_pytester.run(*[sys.executable, "django_project_root/manage.py", "test"])
420+
assert "Found 0 test(s)." in "\n".join(result.outlines)

tests/test_runner.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from unittest.mock import Mock, call
2+
3+
import pytest
4+
5+
from pytest_django.runner import TestRunner
6+
7+
8+
@pytest.mark.parametrize(
9+
"kwargs, expected",
10+
[
11+
({}, call(["tests"])),
12+
({"verbosity": 0}, call(["--quiet", "tests"])),
13+
({"verbosity": 1}, call(["tests"])),
14+
({"verbosity": 2}, call(["-v", "tests"])),
15+
({"verbosity": 3}, call(["-vv", "tests"])),
16+
({"verbosity": 4}, call(["-vvv", "tests"])),
17+
({"failfast": True}, call(["--exitfirst", "tests"])),
18+
({"keepdb": True}, call(["--reuse-db", "tests"])),
19+
],
20+
)
21+
def test_runner_run_tests(monkeypatch, kwargs, expected):
22+
pytest_mock = Mock()
23+
monkeypatch.setattr("pytest.main", pytest_mock)
24+
runner = TestRunner(**kwargs)
25+
runner.run_tests(["tests"])
26+
assert pytest_mock.call_args == expected

0 commit comments

Comments
 (0)