Testing Odoo

There are many ways to test an application. In Odoo, we have three kinds of tests

  • python unit tests: useful for testing model business logic
  • js unit tests: this is necessary to test the javascript code in isolation
  • tours: this is a form of integration testing. The tours ensure that the python and the javascript parts properly talk to each other.

Testing Python code

Odoo provides support for testing modules using unittest.

To write tests, simply define a tests sub-package in your module, it will be automatically inspected for test modules. Test modules should have a name starting with test_ and should be imported from tests/__init__.py, e.g.

your_module
|-- ...
`-- tests
    |-- __init__.py
    |-- test_bar.py
    `-- test_foo.py

and __init__.py contains:

from . import test_foo, test_bar

The test runner will simply run any test case, as described in the official unittest documentation, but Odoo provides a number of utilities and helpers related to testing Odoo content (modules, mainly):

class odoo.tests.common.TransactionCase(methodName='runTest')[source]

TestCase in which each test method is run in its own transaction, and with its own cursor. The transaction is rolled back and the cursor is closed after each test.

browse_ref(xid)[source]

Returns a record object for the provided external identifier

Parameters
xid – fully-qualified external identifier, in the form module.identifier
Raise
ValueError if not found
Returns
BaseModel
ref(xid)[source]

Returns database ID for the provided external identifier, shortcut for get_object_reference

Parameters
xid – fully-qualified external identifier, in the form module.identifier
Raise
ValueError if not found
Returns
registered id
class odoo.tests.common.SingleTransactionCase(methodName='runTest')[source]

TestCase in which all test methods are run in the same transaction, the transaction is started with the first test method and rolled back at the end of the last.

browse_ref(xid)[source]

Returns a record object for the provided external identifier

Parameters
xid – fully-qualified external identifier, in the form module.identifier
Raise
ValueError if not found
Returns
BaseModel
ref(xid)[source]

Returns database ID for the provided external identifier, shortcut for get_object_reference

Parameters
xid – fully-qualified external identifier, in the form module.identifier
Raise
ValueError if not found
Returns
registered id
class odoo.tests.common.SavepointCase(methodName='runTest')[source]

Similar to SingleTransactionCase in that all test methods are run in a single transaction but each test case is run inside a rollbacked savepoint (sub-transaction).

Useful for test cases containing fast tests but with significant database setup common to all cases (complex in-db test data): setUpClass() can be used to generate db test data once, then all test cases use the same data without influencing one another but without having to recreate the test data either.

class odoo.tests.common.HttpCase(methodName='runTest')[source]

Transactional HTTP TestCase with url_open and Chrome headless helpers.

browse_ref(xid)[source]

Returns a record object for the provided external identifier

Parameters
xid – fully-qualified external identifier, in the form module.identifier
Raise
ValueError if not found
Returns
BaseModel
phantom_js(url_path, code, ready='', login=None, timeout=60, **kw)[source]

Test js code running in the browser - optionnally log as ‘login’ - load page given by url_path - wait for ready object to be available - eval(code) inside the page

To signal success test do: console.log(‘ok’)

To signal failure do: console.log(‘error’)

If neither are done before timeout test fails.

ref(xid)[source]

Returns database ID for the provided external identifier, shortcut for get_object_reference

Parameters
xid – fully-qualified external identifier, in the form module.identifier
Raise
ValueError if not found
Returns
registered id
odoo.tests.common.tagged(*tags)[source]

A decorator to tag BaseCase objects Tags are stored in a set that can be accessed from a ‘test_tags’ attribute A tag prefixed by ‘-‘ will remove the tag e.g. to remove the ‘standard’ tag By default, all Test classes from odoo.tests.common have a test_tags attribute that defaults to ‘standard’ and also the module technical name When using class inheritance, the tags are NOT inherited.

By default, tests are run once right after the corresponding module has been installed. Test cases can also be configured to run after all modules have been installed, and not run right after the module installation:

odoo.tests.common.at_install(flag)[source]

Sets the at-install state of a test, the flag is a boolean specifying whether the test should (True) or should not (False) run during module installation.

By default, tests are run right after installing the module, before starting the installation of the next module.

Deprecated since version 12.0: at_install is now a flag, you can use tagged() to add/remove it, although tagged only works on test classes

odoo.tests.common.post_install(flag)[source]

Sets the post-install state of a test. The flag is a boolean specifying whether the test should or should not run after a set of module installations.

By default, tests are not run after installation of all modules in the current installation set.

Deprecated since version 12.0: post_install is now a flag, you can use tagged() to add/remove it, although tagged only works on test classes

The most common situation is to use TransactionCase and test a property of a model in each method:

class TestModelA(common.TransactionCase):
    def test_some_action(self):
        record = self.env['model.a'].create({'field': 'value'})
        record.some_action()
        self.assertEqual(
            record.field,
            expected_field_value)

    # other tests...
class odoo.tests.common.Form(recordp, view=None)[source]

Server-side form view implementation (partial)

Implements much of the “form view” manipulation flow, such that server-side tests can more properly reflect the behaviour which would be observed when manipulating the interface:

  • call default_get and the relevant onchanges on “creation”
  • call the relevant onchanges on setting fields
  • properly handle defaults & onchanges around x2many fields

Saving the form returns the created record if in creation mode.

Regular fields can just be assigned directly to the form, for Many2one fields assign a singleton recordset:

# empty recordset => creation mode
f = Form(self.env['sale.order'])
f.partner_id = a_partner
so = f.save()

When editing a record, using the form as a context manager to automatically save it at the end of the scope:

with Form(so) as f2:
    f2.payment_term_id = env.ref('account.account_payment_term_15days')
    # f2 is saved here

For Many2many fields, the field itself is a M2MProxy and can be altered by adding or removing records:

with Form(user) as u:
    u.groups_id.add(env.ref('account.group_account_manager'))
    u.groups_id.remove(id=env.ref('base.group_portal').id)

Finally One2many are reified as O2MProxy.

Because the One2many only exists through its parent, it is manipulated more directly by creating “sub-forms” with the new() and edit() methods. These would normally be used as context managers since they get saved in the parent record:

with Form(so) as f3:
    # add support
    with f3.order_line.new() as line:
        line.product_id = env.ref('product.product_product_2')
    # add a computer
    with f3.order_line.new() as line:
        line.product_id = env.ref('product.product_product_3')
    # we actually want 5 computers
    with f3.order_line.edit(1) as line:
        line.product_uom_qty = 5
    # remove support
    f3.order_line.remove(index=0)
    # SO is saved here
Parameters
  • recordp (odoo.models.Model) – empty or singleton recordset. An empty recordset will put the view in “creation” mode and trigger calls to default_get and on-load onchanges, a singleton will put it in “edit” mode and only load the view’s data.
  • view (int | str | odoo.model.Model) – the id, xmlid or actual view object to use for onchanges and view constraints. If none is provided, simply loads the default view for the model.

New in version 12.0.

save()[source]

Saves the form, returns the created record if applicable

  • does not save readonly fields
  • does not save unmodified fields (during edition) — any assignment or onchange return marks the field as modified, even if set to its current value
Raises
AssertionError – if the form has any unfilled required field
class odoo.tests.common.M2MProxy[source]

Behaves as a Sequence of recordsets, can be indexed or sliced to get actual underlying recordsets.

add(record)[source]

Adds record to the field, the record must already exist.

The addition will only be finalized when the parent record is saved.

clear()[source]

Removes all existing records in the m2m

remove(id=None, index=None)[source]

Removes a record at a certain index or with a provided id from the field.

class odoo.tests.common.O2MProxy[source]
edit(index)[source]

Returns a Form to edit the pre-existing One2many record.

The form is created from the list view if editable, or the field’s form view otherwise.

Raises
AssertionError – if the field is not editable
new()[source]

Returns a Form for a new One2many record, properly initialised.

The form is created from the list view if editable, or the field’s form view otherwise.

Raises
AssertionError – if the field is not editable
remove(index)[source]

Removes the record at index from the parent form.

Raises
AssertionError – if the field is not editable

Running tests

Tests are automatically run when installing or updating modules if --test-enable was enabled when starting the Odoo server.

Test selection

In Odoo, Python tests can be tagged to facilitate the test selection when running tests.

Subclasses of odoo.tests.common.BaseCase (usually through TransactionCase, SavepointCase or HttpCase) are automatically tagged with standard, at_install and their source module’s name by default.

Invocation

--test-tags can be used to select/filter tests to run on the command-line.

This option defaults to +standard meaning tests tagged standard (explicitly or implicitly) will be run by default when starting Odoo with --test-enable.

When writing tests, the tagged() decorator can be used on test classes to add or remove tags.

The decorator’s arguments are tag names, as strings.

Tags can be prefixed with the minus (-) sign, to remove them instead of add or select them e.g. if you don’t want your test to be executed by default you can remove the standard tag:

from odoo.tests import TransactionCase, tagged

@tagged('-standard', 'nice')
class NiceTest(TransactionCase):
    ...

This test will not be selected by default, to run it the relevant tag will have to be selected explicitely:

$ odoo-bin --test-enable --test-tags nice

Note that only the tests tagged nice are going to be executed. To run both nice and standard tests, provide multiple values to --test-tags: on the command-line, values are additive (you’re selecting all tests with any of the specified tags)

$ odoo-bin --test-enable --test-tags nice,standard

The config switch parameter also accepts the + and - prefixes. The + prefix is implied and therefore, totaly optional. The - (minus) prefix is made to deselect tests tagged with the prefixed tags, even if they are selected by other specified tags e.g. if there are standard tests which are also tagged as slow you can run all standard tests except the slow ones:

$ odoo-bin --test-enable --test-tags 'standard,-slow'

When you write a test that does not inherit from the BaseCase, this test will not have the default tags, you have to add them explicitely to have the test included in the default test suite. This is a common issue when using a simple unittest.TestCase as they’re not going to get run:

import unittest
from odoo.tests import tagged

@tagged('standard', 'at_install')
class SmallTest(unittest.TestCase):
    ...

Special tags

  • standard: All Odoo tests that inherit from BaseCase are implicitely tagged standard. --test-tags also defaults to standard.

    That means untagged test will be executed by default when tests are enabled.

  • at_install: Means that the test will be executed right after the module installation and before other modules are installed. This is a default implicit tag.
  • post_install: Means that the test will be executed after all the modules are installed. This is what you want for HttpCase tests most of the time.

    Note that this is not exclusive with at_install, however since you will generally not want both post_install is usually paired with -at_install when tagging a test class.

  • module_name: Odoo tests classes extending BaseCase are implicitely tagged with the technical name of their module. This allows easily selecting or excluding specific modules when testing e.g. if you want to only run tests from stock_account:

    $ odoo-bin --test-enable --test-tags stock_account
    

Examples

Run only the tests from the sale module:

$ odoo-bin --test-enable --test-tags sale

Run the tests from the sale module but not the ones tagged as slow:

$ odoo-bin --test-enable --test-tags 'sale,-slow'

Run only the tests from stock or tagged as slow:

$ odoo-bin --test-enable --test-tags '-standard, slow, stock'

Testing JS code

Qunit test suite

Odoo Web includes means to unit-test both the core code of Odoo Web and your own javascript modules. On the javascript side, unit-testing is based on QUnit with a number of helpers and extensions for better integration with Odoo.

To see what the runner looks like, find (or start) an Odoo server with the web client enabled, and navigate to /web/tests This will show the runner selector, which lists all modules with javascript unit tests, and allows starting any of them (or all javascript tests in all modules at once).

Clicking any runner button will launch the corresponding tests in the bundled QUnit runner:

Writing a test case

This section will be updated as soon as possible.

Integration Testing

Testing Python code and JS code separately is very useful, but it does not prove that the web client and the server work together. In order to do that, we can write another kind of test: tours. A tour is a mini scenario of some interesting business flow. It explains a sequence of steps that should be followed. The test runner will then create a phantom_js browser, point it to the proper url and simulate the click and inputs, according to the scenario.