diff --git a/docs/faq.rst b/docs/faq.rst index 3489ce05..8ad588b0 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -79,53 +79,12 @@ How can I use ``manage.py test`` with pytest-django? ---------------------------------------------------- pytest-django is designed to work with the ``pytest`` command, but if you -really need integration with ``manage.py test``, you can create a simple -test runner like this: +really need integration with ``manage.py test``, you can add this class path +in your Django settings: .. code-block:: python - class PytestTestRunner: - """Runs pytest to discover and run tests.""" - - def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs): - self.verbosity = verbosity - self.failfast = failfast - self.keepdb = keepdb - - @classmethod - def add_arguments(cls, parser): - parser.add_argument( - '--keepdb', action='store_true', - help='Preserves the test DB between runs.' - ) - - def run_tests(self, test_labels, **kwargs): - """Run pytest and return the exitcode. - - It translates some of Django's test command option to pytest's. - """ - import pytest - - argv = [] - if self.verbosity == 0: - argv.append('--quiet') - if self.verbosity == 2: - argv.append('--verbose') - if self.verbosity == 3: - argv.append('-vv') - if self.failfast: - argv.append('--exitfirst') - if self.keepdb: - argv.append('--reuse-db') - - argv.extend(test_labels) - return pytest.main(argv) - -Add the path to this class in your Django settings: - -.. code-block:: python - - TEST_RUNNER = 'my_project.runner.PytestTestRunner' + TEST_RUNNER = 'pytest_django.runner.TestRunner' Usage: diff --git a/pytest_django/runner.py b/pytest_django/runner.py new file mode 100644 index 00000000..d9032622 --- /dev/null +++ b/pytest_django/runner.py @@ -0,0 +1,45 @@ +from argparse import ArgumentParser +from typing import Any, Iterable + + +class TestRunner: + """A Django test runner which uses pytest to discover and run tests when using `manage.py test`.""" + + def __init__( + self, + *, + verbosity: int = 1, + failfast: bool = False, + keepdb: bool = False, + **kwargs: Any, + ) -> None: + self.verbosity = verbosity + self.failfast = failfast + self.keepdb = keepdb + + @classmethod + def add_arguments(cls, parser: ArgumentParser) -> None: + parser.add_argument( + "--keepdb", action="store_true", help="Preserves the test DB between runs." + ) + + def run_tests(self, test_labels: Iterable[str], **kwargs: Any) -> int: + """Run pytest and return the exitcode. + + It translates some of Django's test command option to pytest's. + """ + import pytest + + argv = [] + if self.verbosity == 0: + argv.append("--quiet") + elif self.verbosity >= 2: + verbosity = "v" * (self.verbosity - 1) + argv.append(f"-{verbosity}") + if self.failfast: + argv.append("--exitfirst") + if self.keepdb: + argv.append("--reuse-db") + + argv.extend(test_labels) + return pytest.main(argv) diff --git a/tests/conftest.py b/tests/conftest.py index c8f485c1..16e209f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -117,7 +117,16 @@ def django_pytester( tpkg_path.mkdir() if options["create_manage_py"]: - project_root.joinpath("manage.py").touch() + project_root.joinpath("manage.py").write_text( + dedent( + """ + #!/usr/bin/env python + import sys + from django.core.management import execute_from_command_line + execute_from_command_line(sys.argv) + """ + ) + ) tpkg_path.joinpath("__init__.py").touch() diff --git a/tests/test_environment.py b/tests/test_environment.py index a3549732..5c4e4292 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1,4 +1,5 @@ import os +import sys import pytest from django.contrib.sites import models as site_models @@ -308,7 +309,6 @@ class TestrunnerVerbosity: @pytest.fixture def pytester(self, django_pytester: DjangoPytester) -> pytest.Pytester: - print("pytester") django_pytester.create_test_module( """ import pytest @@ -379,3 +379,42 @@ def test_clear_site_cache_check_site_cache_size(site_name: str, settings) -> Non settings.SITE_ID = site.id assert Site.objects.get_current() == site assert len(site_models.SITE_CACHE) == 1 + + +@pytest.mark.django_project( + project_root="django_project_root", + create_manage_py=True, + extra_settings=""" + TEST_RUNNER = 'pytest_django.runner.TestRunner' + """, +) +def test_manage_test_runner(django_pytester: DjangoPytester) -> None: + django_pytester.create_test_module( + """ + import pytest + + @pytest.mark.django_db + def test_inner_testrunner(): + pass + """ + ) + result = django_pytester.run(*[sys.executable, "django_project_root/manage.py", "test"]) + assert "1 passed" in "\n".join(result.outlines) + + +@pytest.mark.django_project( + project_root="django_project_root", + create_manage_py=True, +) +def test_manage_test_runner_without(django_pytester: DjangoPytester) -> None: + django_pytester.create_test_module( + """ + import pytest + + @pytest.mark.django_db + def test_inner_testrunner(): + pass + """ + ) + result = django_pytester.run(*[sys.executable, "django_project_root/manage.py", "test"]) + assert "Found 0 test(s)." in "\n".join(result.outlines) diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 00000000..71fd7160 --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,26 @@ +from unittest.mock import Mock, call + +import pytest + +from pytest_django.runner import TestRunner + + +@pytest.mark.parametrize( + "kwargs, expected", + [ + ({}, call(["tests"])), + ({"verbosity": 0}, call(["--quiet", "tests"])), + ({"verbosity": 1}, call(["tests"])), + ({"verbosity": 2}, call(["-v", "tests"])), + ({"verbosity": 3}, call(["-vv", "tests"])), + ({"verbosity": 4}, call(["-vvv", "tests"])), + ({"failfast": True}, call(["--exitfirst", "tests"])), + ({"keepdb": True}, call(["--reuse-db", "tests"])), + ], +) +def test_runner_run_tests(monkeypatch, kwargs, expected): + pytest_mock = Mock() + monkeypatch.setattr("pytest.main", pytest_mock) + runner = TestRunner(**kwargs) + runner.run_tests(["tests"]) + assert pytest_mock.call_args == expected