Skip to content

Commit fb04766

Browse files
committed
[IMP] tests: document testing framework
Add details about the test classes for upgrades. See: odoo/documentation#14649, the patch here should be merged first.
1 parent badd3d8 commit fb04766

File tree

1 file changed

+195
-34
lines changed

1 file changed

+195
-34
lines changed

src/testing.py

Lines changed: 195 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,112 @@
1+
r"""
2+
There are two main classes used testing during the upgrade.
3+
4+
* :class:`~odoo.upgrade.testing.UpgradeCase` for testing upgrade scripts,
5+
* :class:`~odoo.upgrade.testing.UpgradeCase` for testing invariants across versions.
6+
7+
Subclasses must implement:
8+
9+
* For ``UpgradeCase``:
10+
11+
- ``prepare`` method: prepare data before upgrade,
12+
- ``check`` method: check data was correctly upgraded.
13+
14+
* For ``IntegrityCase``:
15+
16+
- ``invariant`` method: compute an invariant to check.
17+
18+
Put your test classes in a ``tests`` Python module (folder) in any of the folders
19+
containing the upgrade scripts of your modules. The script containing your tests must
20+
have a `test_` prefix. The ``tests`` module must contain an ``__init__.py`` file to be
21+
detected by Odoo.
22+
23+
Example directory structure::
24+
25+
myupgrades/
26+
└── mymodule1/
27+
├── 18.0.1.1.2/
28+
│ └── pre-myupgrade.py
29+
└── tests/
30+
├── __init__.py
31+
└── test_myupgrade.py
32+
33+
.. note::
34+
35+
The tests in the example above will be loaded only if ``mymodule1`` is being
36+
**upgraded.**
37+
38+
Running Upgrade Tests
39+
---------------------
40+
41+
After receiving an upgraded database with all standard Odoo modules already upgraded to
42+
their target version, you can test the upgrade of custom modules by following a three-step
43+
process:
44+
45+
1. Prepare the test data
46+
47+
.. code-block:: bash
48+
49+
$ ~/odoo/$version/odoo-bin -d DB --test-tags=$prepare_test_tag \
50+
--upgrade-path=~/upgrade-util/src,~/myupgrades \
51+
--addons=~/odoo/$version/addons,~/enterprise/$version,~/mymodules --stop
52+
53+
2. Upgrade the modules
54+
55+
.. code-block:: bash
56+
57+
$ ~/odoo/$version/odoo-bin -d DB -u mymodule1,mymodule2 \
58+
--upgrade-path=~/upgrade-util/src,~/myupgrades \
59+
--addons=~/odoo/$version/addons,~/enterprise/$version,~/mymodules --stop
60+
61+
3. Check the upgraded data
62+
63+
.. code-block:: bash
64+
65+
$ ~/odoo/$version/odoo-bin -d DB --test-tags=$check_test_tag \
66+
--upgrade-path=~/upgrade-util/src,~/myupgrades \
67+
--addons=~/odoo/$version/addons,~/enterprise/$version,~/mymodules --stop
68+
69+
The example above assumes that ``$version`` is the target version of your upgrade (e.g.
70+
``18.0``), ``DB`` is the name of your database, and ``mymodule1,mymodule2`` are the
71+
modules you want to upgrade. The directory structure assumes that ``~/odoo/$version`` and
72+
``~/enterprise/$version`` contain the Community and Enterprise source code for the target
73+
Odoo version, respectively. ``~/mymodules`` contains the code of your custom modules
74+
(``mymodule1``, ...), ``~/myupgrades`` contains your custom upgrade scripts, and
75+
``~/upgrade-util`` contains the `upgrade utils <https://github.com/odoo/upgrade-util/>`_
76+
repo.
77+
78+
The variables ``$prepare_test_tag`` and ``$check_test_tag`` must be set according to:
79+
80+
.. list-table::
81+
:header-rows: 1
82+
:stub-columns: 1
83+
84+
* - Variable
85+
- ``UpgradeCase``
86+
- ``IntegrityCase``
87+
* - ``$prepare_test_tag``
88+
- ``upgrade.test_prepare``
89+
- ``integrity_case.test_prepare``
90+
* - ``$check_test_tag``
91+
- ``upgrade.test_check``
92+
- ``integrity_test.test_check``
93+
94+
.. note::
95+
96+
`upgrade.test_prepare` also runs ``IntegrityCase`` tests, so you can prepare data
97+
for both ``UpgradeCase`` and ``IntegrityCase`` tests with only this tag.
98+
99+
.. warning::
100+
101+
Do **not** run any ``prepare`` method of an ``UpgradeCase`` before sending your
102+
database for a **production** upgrade to `upgrade.odoo.com
103+
<https://upgrade.odoo.com>`_. Doing so may risk your upgrade being blocked and marked
104+
as failed.
105+
106+
API documentation
107+
-----------------
108+
"""
109+
1110
import functools
2111
import inspect
3112
import logging
@@ -40,22 +149,17 @@ def parametrize(argvalues):
40149
"""
41150
Parametrize a test function.
42151
43-
Decorator for UnitTestCase test functions to parametrize the decorated test.
44-
45-
Usage:
46-
```python
47-
@parametrize([
48-
(1, 2),
49-
(2, 4),
50-
(-1, -2),
51-
(0, 0),
52-
])
53-
def test_double(self, input, expected):
54-
self.assertEqual(input * 2, expected)
55-
```
56-
57-
It works by injecting test functions in the containing class.
58-
Idea taken from the `parameterized` package (https://pypi.org/project/parameterized/).
152+
Decorator for upgrade test functions to parametrize and generate multiple tests from
153+
it.
154+
155+
Usage::
156+
157+
@parametrize([(1, 2), (2, 4), (-1, -2), (0, 0)])
158+
def test_double(self, input, expected):
159+
self.assertEqual(input * 2, expected)
160+
161+
Works by injecting test functions in the containing class.
162+
Inspired by the `parameterized <https://pypi.org/project/parameterized/>`_ package.
59163
"""
60164

61165
def make_func(func, name, args):
@@ -116,6 +220,8 @@ def __init_subclass__(cls):
116220

117221

118222
class UnitTestCase(TransactionCase, _create_meta(10, "upgrade_unit")):
223+
""":meta private: exclude from online docs."""
224+
119225
@classmethod
120226
def setUpClass(cls):
121227
super().setUpClass()
@@ -211,6 +317,8 @@ def assertUpdated(self, table, ids=None, msg=None):
211317

212318

213319
class UpgradeCommon(BaseCase):
320+
""":meta private: exclude from online docs."""
321+
214322
__initialized = False
215323

216324
change_version = (None, None)
@@ -356,6 +464,25 @@ def convert_check(self, value):
356464

357465

358466
def change_version(version_str):
467+
"""
468+
Class decorator to specify the version on which a test is relevant.
469+
470+
Using ``@change_version(version)`` indicates:
471+
472+
* ``test_prepare`` will only run if the current Odoo version is in the range
473+
``[next_major_version-1, version)``.
474+
* ``test_check`` will only run if the current Odoo version is in the range ``[version,
475+
next_major_version)``.
476+
477+
``next_major_version`` is the next major version after ``version``, e.g. for
478+
``saas~17.2`` it is ``18.0``.
479+
480+
.. note::
481+
482+
Do not use this decorator if your upgrade is in the same major version. Otherwise,
483+
your tests will not run.
484+
"""
485+
359486
def version_decorator(obj):
360487
match = VERSION_RE.match(version_str)
361488
if not match:
@@ -394,24 +521,42 @@ def get_previous_major(major, minor):
394521
# pylint: disable=inherit-non-class
395522
class UpgradeCase(UpgradeCommon, _create_meta(10, "upgrade_case")):
396523
"""
397-
Test case to modify data in origin version, and assert in target version.
524+
Test case to verify that the upgrade scripts correctly upgrade data.
398525
399-
User must define a "prepare" and a "check" method.
400-
- prepare method can write in database, return value will be stored in a dedicated table and
401-
passed as argument to check.
402-
- check method can assert that the received argument is the one expected,
403-
executing any code to retrieve equivalent information in migrated database.
404-
Note: check argument is a loaded json dump, meaning that tuple are converted to list.
405-
convert_check can be used to normalise the right part of the comparison.
526+
Override:
406527
407-
check method is only called if corresponding prepared was run in previous version
528+
* ``prepare`` to set up data,
529+
* ``check`` to assert expectations after the upgrade.
408530
409-
prepare and check implementation may contains version conditional code to match api changes.
531+
The ORM can be used in these methods to perform the functional flow under test. The
532+
return value of ``prepare`` is persisted and passed as an argument to ``check``. It
533+
must be JSON-serializable.
410534
411-
using @change_version class decorator can indicate with script version is tested here if any:
412-
Example: to test a saas~12.3 script, using @change_version('saas-12,3') will only run prepare if
413-
version in [12.0, 12.3[ and run check if version is in [12.3, 13]
535+
.. note::
414536
537+
Since ``prepare`` injects or modifies data, this type of test is intended **only
538+
for development**. Use it to test upgrade scripts while developing them. **Do not**
539+
run these tests for a production upgrade. To verify that upgrades preserved
540+
important invariants in production, use ``IntegrityCase`` tests instead.
541+
542+
.. example::
543+
544+
.. code-block:: python
545+
546+
from odoo.upgrade.testing import UpgradeCase, change_version
547+
548+
549+
class DeactivateBobUsers(UpgradeCase):
550+
551+
def prepare(self):
552+
u = self.env["res.users"].create({"login": "bob", "name": "Bob"})
553+
return u.id # will be passed to check
554+
555+
def check(self, uid): # uid is the value returned by prepare
556+
self.env.cr.execute(
557+
"SELECT * FROM res_users WHERE id=%s AND NOT active", [uid]
558+
)
559+
self.assertEqual(self.env.cr.rowcount, 1)
415560
"""
416561

417562
def __init_subclass__(cls, abstract=False):
@@ -427,12 +572,27 @@ def test_prepare(self):
427572
# pylint: disable=inherit-non-class
428573
class IntegrityCase(UpgradeCommon, _create_meta(20, "integrity_case")):
429574
"""
430-
Test case to check invariant through any version.
575+
Test case for validating invariants across upgrades.
576+
577+
Override:
578+
579+
* ``invariant`` to return a JSON-serializable value representing
580+
the invariant to check.
581+
582+
The ``invariant`` method is called both before and after the upgrade,
583+
and the results are compared.
584+
585+
586+
.. example::
587+
588+
.. code-block:: python
589+
590+
from odoo.upgrade.testing import IntegrityCase
431591
432-
User must define a "invariant" method.
433-
invariant return value will be compared between the two version.
434592
435-
invariant implementation may contains version conditional code to match api changes.
593+
class NoNewUsers(IntegrityCase):
594+
def invariant(self):
595+
return self.env["res.users"].search_count([])
436596
"""
437597

438598
message = "Invariant check fail"
@@ -442,7 +602,7 @@ def __init_subclass__(cls, abstract=False):
442602
if not abstract and not hasattr(cls, "invariant"):
443603
_logger.error("%s (IntegrityCase) must define an invariant method", cls.__name__)
444604

445-
# IntegrityCase should not alterate database:
605+
# IntegrityCase should not alter the database:
446606
# TODO give a test cursor, don't commit after prepare, use a protected cursor to set_value
447607

448608
def prepare(self):
@@ -462,6 +622,7 @@ def _setup_registry(self):
462622
self.addCleanup(self.registry.leave_test_mode)
463623

464624
def setUp(self):
625+
""":meta private: exclude from online docs."""
465626
super(IntegrityCase, self).setUp()
466627

467628
def commit(self):

0 commit comments

Comments
 (0)