From 3260fbbe7c4a65a06abe89e15f29c1550929aafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 22 Sep 2022 17:28:37 +0200 Subject: [PATCH] Use pytest as the test runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pytest has several benefits over unittest: - captures stdout/stderr, restoring the ability to get an overview of the test run progress. To further improve the test run overview, the runner verbosity was decreased. - shows detailed reports for failures and errors The invocation is also much easier. Before this change: ``` DJANGO_SETTINGS_MODULE=config.settings.test django-admin --no-input --keepdb --parallel ``` With pytest (and a pytest.ini configuration), invocation is simply: ``` pytest ``` For more on pytest, see https://pytest-django.readthedocs.io/en/latest/index.html#why-would-i-use-this-instead-of-django-s-manage-py-test-command The `--failfast` option was not ported. It’s viewed as a hindrance by most team members, having to run the CI multiple times to get a full test report. With the simplified invocation, the `make test-interactive` recipe was no longer useful and was dropped. Also, passing arguments to the test runner (such as `-k TestFilter`) is also eased compared to the previous make TARGET variable. With make: ``` make test TARGET="-k TestFilter" ``` With pytest: ``` pytest -k TestFilter ``` Pytests parallel test runner splits the execution at the individual test level, whereas Django runner (based on unittest) splits at the TestCase level. That caused an issue with ImportSiaeManagementCommandsTest, which setup a test directory that would be shared by pytest workers. Update the test suite to use a temporary directory, specific to the worker process. Finally, add pytest-xdist as a dependency to support running the test suite in parallel. --- .github/workflows/ci.yml | 3 ++- Makefile | 14 ++++---------- itou/siaes/tests/tests_import_siae_command.py | 16 +++++++++++----- pytest.ini | 2 +- requirements/dev.in | 1 + requirements/dev.txt | 18 +++++++++++++++++- 6 files changed, 36 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8488899acc..a856b8a5e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,7 @@ jobs: - name: 🚧 Check pending migrations run: django-admin makemigrations --check --dry-run --noinput - name: 🤹‍ Django tests - run: django-admin test --noinput --failfast --parallel + run: make test env: DJANGO_DEBUG: True + USE_VENV: 1 diff --git a/Makefile b/Makefile index a9c25d3ab3..9616526ea9 100644 --- a/Makefile +++ b/Makefile @@ -112,19 +112,13 @@ graph_models_itou: # Tests. # ============================================================================= -.PHONY: test test-interactive coverage +.PHONY: coverage test -TEST_OPTS += --force-color --timing --no-input - -test: - $(EXEC_CMD) ./manage.py test --settings=config.settings.test $(TEST_OPTS) --parallel $(TARGET) - -# Lets you add a debugger. -test-interactive: - $(EXEC_CMD) ./manage.py test --settings=config.settings.test $(TEST_OPTS) --keepdb $(TARGET) +test: $(VIRTUAL_ENV) + $(EXEC_CMD) pytest $(TARGET) coverage: - $(EXEC_CMD) coverage run ./manage.py test itou --settings=config.settings.test --no-input + $(EXEC_CMD) coverage run -m pytest $(EXEC_CMD) coverage html # Docker shell. diff --git a/itou/siaes/tests/tests_import_siae_command.py b/itou/siaes/tests/tests_import_siae_command.py index 6225499732..13e57aeb0c 100644 --- a/itou/siaes/tests/tests_import_siae_command.py +++ b/itou/siaes/tests/tests_import_siae_command.py @@ -2,8 +2,10 @@ import importlib import os import shutil +import tempfile import unittest from pathlib import Path +from unittest import mock from django.conf import settings from django.core import mail @@ -42,7 +44,6 @@ def lazy_import_siae_command(): ) class ImportSiaeManagementCommandsTest(TransactionTestCase): - path_dest = "./siaes/management/commands/data" path_source = "./siaes/fixtures" app_dir_path = Path((settings.APPS_DIR)) mod = None @@ -52,17 +53,22 @@ def setUpClass(cls): """We need to setup fake files before loading any `import_siae` related script, since it does rely on dynamic file loading upon startup (!) """ - # copying datasets from fixtures dir + path_dest = tempfile.mkdtemp() + cls.addClassCleanup(shutil.rmtree, path_dest) + data_dir = Path(path_dest) / "data" + data_dir.mkdir() + data_dir_mock = mock.patch("itou.siaes.management.commands._import_siae.utils.CURRENT_DIR", data_dir) + data_dir_mock.start() + cls.addClassCleanup(data_dir_mock.stop) + files = [x for x in cls.app_dir_path.joinpath(cls.path_source).glob("fluxIAE_*.csv.gz") if x.is_file()] - cls.app_dir_path.joinpath(cls.path_dest).mkdir(parents=True, exist_ok=True) for file in files: - shutil.copy(file, cls.app_dir_path.joinpath(cls.path_dest)) + shutil.copy(file, data_dir) cls.mod = importlib.import_module("itou.siaes.management.commands._import_siae.convention") @classmethod def tearDownClass(cls): - shutil.rmtree(cls.app_dir_path.joinpath(cls.path_dest)) cls.mod = None def test_uncreatable_conventions_for_active_siae_with_active_convention(self): diff --git a/pytest.ini b/pytest.ini index 904ec6cc73..125cd8dc4c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] DJANGO_SETTINGS_MODULE = config.settings.test python_files = test*.py -addopts = --reuse-db --verbose --capture=no +addopts = --numprocesses=logical --reuse-db diff --git a/requirements/dev.in b/requirements/dev.in index 4c9ea739d8..035c931d06 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -39,6 +39,7 @@ requests-mock==1.9.3 # https://github.com/jamielennox/requests-mock respx==0.19.2 # https://lundberg.github.io/respx/ pytest==7.1.2 # https://github.com/pytest-dev/pytest pytest-django== 4.5.2 # https://github.com/pytest-dev/pytest-django/ +pytest-xdist # https://pypi.org/project/pytest-xdist/ # Data extracts # ------------------------------------------------------------------------------ diff --git a/requirements/dev.txt b/requirements/dev.txt index 0e459bbdc8..0c09182a2b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -365,6 +365,10 @@ et-xmlfile==1.1.0 \ --hash=sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c \ --hash=sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada # via openpyxl +execnet==1.9.0 \ + --hash=sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5 \ + --hash=sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142 + # via pytest-xdist executing==1.0.0 \ --hash=sha256:550d581b497228b572235e633599133eeee67073c65914ca346100ad56775349 \ --hash=sha256:98daefa9d1916a4f0d944880d5aeaf079e05585689bebd9ff9b32e31dd5e1017 @@ -897,7 +901,9 @@ pure-eval==0.2.2 \ py==1.11.0 \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via pytest + # via + # pytest + # pytest-forked pycodestyle==2.8.0 \ --hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \ --hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f @@ -983,10 +989,20 @@ pytest==7.1.2 \ # via # -r requirements/dev.in # pytest-django + # pytest-forked + # pytest-xdist pytest-django==4.5.2 \ --hash=sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e \ --hash=sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2 # via -r requirements/dev.in +pytest-forked==1.4.0 \ + --hash=sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e \ + --hash=sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8 + # via pytest-xdist +pytest-xdist==2.5.0 \ + --hash=sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf \ + --hash=sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65 + # via -r requirements/dev.in python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9