diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f447421..ae94cc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,8 @@ jobs: os: ubuntu-latest - PYTHON_VERSION: "3.12" os: ubuntu-latest + - PYTHON_VERSION: "3.13" + os: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python diff --git a/README.rst b/README.rst index 77ef676..03254cf 100644 --- a/README.rst +++ b/README.rst @@ -53,3 +53,5 @@ You can use the ``ODOO_RC`` environment variable using an odoo configuration fil The plugin is also compatible with distributed run provided by the `pytest-xdist `_ library. When tests are distributed, a copy of the database is created for each worker at the start of the test session. This is useful to avoid concurrent access to the same database, which can lead to deadlocks. The provided database is therefore used only as template. At the end of the tests, all the created databases are dropped. + +The plugin is also compatible with `pytest-subtests `_ library. When test use the `subTest` context manager you'll get a nice output for each sub-tests failing. diff --git a/pytest_odoo.py b/pytest_odoo.py index 57d41ed..f92d223 100644 --- a/pytest_odoo.py +++ b/pytest_odoo.py @@ -10,9 +10,10 @@ import subprocess import threading from contextlib import contextmanager -from unittest import mock from pathlib import Path from typing import Optional +from unittest import TestCase as UnitTestTestCase +from unittest import mock import _pytest import _pytest.python @@ -88,6 +89,7 @@ def pytest_cmdline_main(config): raise Exception( "please provide a database name in the Odoo configuration file" ) + support_subtest() disable_odoo_test_retry() monkey_patch_resolve_pkg_root_and_module_name() odoo.service.server.start(preload=[], stop=True) @@ -105,7 +107,6 @@ def pytest_cmdline_main(config): else: yield - @pytest.fixture(scope="module", autouse=True) def load_http(request): if request.config.getoption("--odoo-http"): @@ -152,7 +153,7 @@ def _worker_db_name(): odoo.tools.config["db_name"] = original_db_name odoo.tools.config["dbfilter"] = f"^{original_db_name}$" - + @pytest.fixture(scope='session', autouse=True) def load_registry(): # Initialize the registry before running tests. @@ -202,6 +203,22 @@ def resolve_pkg_root_and_module_name( _pytest.pathlib.resolve_pkg_root_and_module_name= resolve_pkg_root_and_module_name +def support_subtest(): + """Odoo from version 16.0 re-define its own TestCase.subTest context manager + + Odoo assume the usage of OdooTestResult which is not our case + using with pytest-odoo. So this fallback to the unitest.TestCase.subTest + Context manager + """ + try: + from odoo.tests.case import TestCase + TestCase.subTest = UnitTestTestCase.subTest + TestCase.run = UnitTestTestCase.run + except ImportError: + # Odoo <= 15.0 + pass + + def disable_odoo_test_retry(): """Odoo BaseCase.run method overload TestCase.run and manage a retry mechanism that breaks using pytest launcher. @@ -215,6 +232,7 @@ def disable_odoo_test_retry(): # Odoo <= 15.0 pass + def _find_manifest_path(collection_path: Path) -> Path: """Try to locate an Odoo manifest file in the collection path.""" # check if collection_path is an addon directory diff --git a/tests/mock/odoo/odoo/tests/__init__.py b/tests/mock/odoo/odoo/tests/__init__.py index cda5288..c2980c4 100644 --- a/tests/mock/odoo/odoo/tests/__init__.py +++ b/tests/mock/odoo/odoo/tests/__init__.py @@ -1,8 +1,13 @@ +from . import case + from unittest.mock import MagicMock common = MagicMock() +class BaseCase(case.TestCase): -class BaseCase: - - def run(*args, **kwargs): + def run(self, *args, **kwargs): super().run(*args, **kwargs) + self._call_something() + + def _call_something(self): + pass diff --git a/tests/mock/odoo/odoo/tests/case.py b/tests/mock/odoo/odoo/tests/case.py new file mode 100644 index 0000000..4d45f38 --- /dev/null +++ b/tests/mock/odoo/odoo/tests/case.py @@ -0,0 +1,14 @@ +import contextlib + + +class TestCase: + + @contextlib.contextmanager + def subTest(self, **kwargs): + """Simulate odoo TestCase.subTest from version 15.0""" + + def run(self, *args, **kwargs): + self._call_a_method() + + def _call_a_method(self): + pass diff --git a/tests/test_pytest_odoo.py b/tests/test_pytest_odoo.py index bf675c8..7f9e622 100644 --- a/tests/test_pytest_odoo.py +++ b/tests/test_pytest_odoo.py @@ -2,12 +2,14 @@ from contextlib import contextmanager from pathlib import Path from unittest import TestCase +from unittest.mock import patch from _pytest import pathlib as pytest_pathlib from pytest_odoo import ( _find_manifest_path, - monkey_patch_resolve_pkg_root_and_module_name, disable_odoo_test_retry, + monkey_patch_resolve_pkg_root_and_module_name, + support_subtest, ) @@ -93,11 +95,13 @@ def test_disable_odoo_test_retry(self): def restore_basecase_run(): BaseCase.run = original_basecase_run - + self.addCleanup(restore_basecase_run) disable_odoo_test_retry() - self.assertFalse(hasattr(BaseCase, "run")) + with patch("odoo.tests.BaseCase._call_something") as mock: + BaseCase().run() + mock.assert_not_called() def test_disable_odoo_test_retry_ignore_run_doesnt_exists(self): @@ -107,25 +111,57 @@ def test_disable_odoo_test_retry_ignore_run_doesnt_exists(self): def restore_basecase_run(): BaseCase.run = original_basecase_run - + self.addCleanup(restore_basecase_run) - + del BaseCase.run - - disable_odoo_test_retry() - self.assertFalse(hasattr(BaseCase, "run")) + disable_odoo_test_retry() + with patch("odoo.tests.BaseCase._call_something") as mock: + BaseCase().run() + mock.assert_not_called() def test_import_error(self): - from odoo import tests - + from odoo import tests + original_BaseCase = tests.BaseCase def restore_basecase(): tests.BaseCase = original_BaseCase - + self.addCleanup(restore_basecase) - + + del tests.BaseCase disable_odoo_test_retry() - \ No newline at end of file + + def test_support_subtest(self): + from odoo.tests import case + + original_test_case = case.TestCase + + def restore(): + case.TestCase = original_test_case + + self.addCleanup(restore) + support_subtest() + + from odoo.tests import BaseCase + from odoo.tests.case import TestCase as OdooTestCase + + self.assertTrue(OdooTestCase.subTest is TestCase.subTest) + self.assertTrue(BaseCase.subTest is TestCase.subTest) + self.assertTrue(OdooTestCase.run is TestCase.run) + + def test_support_subtest_import_error(self): + from odoo.tests import case + + original_odoo_test_case = case.TestCase + + def restore_testcase(): + case.TestCase = original_odoo_test_case + + self.addCleanup(restore_testcase) + + del case.TestCase + support_subtest()