Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 7 additions & 244 deletions content/developer/reference/backend/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,252 +281,17 @@ Testing JS code

Testing a complex system is an important safeguard to prevent regressions and to
guarantee that some basic functionality still works. Since Odoo has a non trivial
codebase in Javascript, it is necessary to test it. In this section, we will
discuss the practice of testing JS code in isolation: these tests stay in the
browser, and are not supposed to reach the server.
codebase in Javascript, it is necessary to test it.

.. _reference/testing/qunit:
See the :doc:`Unit testing <../frontend/unit_testing>` to learn about the
various aspect of the front-end testing framework, or jump directly to one of the
sub-sections:

Qunit test suite
----------------
- :doc:`Hoot <../frontend/unit_testing/hoot>`

The Odoo framework uses the QUnit_ library testing framework as a test runner.
QUnit defines the concepts of *tests* and *modules* (a set of related tests),
and gives us a web based interface to execute the tests.
- :doc:`Web test helpers <../frontend/unit_testing/web_helpers>`

For example, here is what a pyUtils test could look like:

.. code-block:: javascript

QUnit.module('py_utils');

QUnit.test('simple arithmetic', function (assert) {
assert.expect(2);

var result = pyUtils.py_eval("1 + 2");
assert.strictEqual(result, 3, "should properly evaluate sum");
result = pyUtils.py_eval("42 % 5");
assert.strictEqual(result, 2, "should properly evaluate modulo operator");
});

The main way to run the test suite is to have a running Odoo server, then
navigate a web browser to ``/web/tests``. The test suite will then be executed
by the web browser Javascript engine.

.. image:: testing/tests.png
:align: center

The web UI has many useful features: it can run only some submodules, or
filter tests that match a string. It can show every assertions, failed or passed,
rerun specific tests, ...

.. warning::

While the test suite is running, make sure that:

- your browser window is focused,
- it is not zoomed in/out. It needs to have exactly 100% zoom level.

If this is not the case, some tests will fail, without a proper explanation.

Testing Infrastructure
----------------------

Here is a high level overview of the most important parts of the testing
infrastructure:

- there is an asset bundle named `web.qunit_suite`_. This bundle contains
the main code (assets common + assets backend), some libraries, the QUnit test
runner and the test bundles listed below.

- a bundle named `web.tests_assets`_ includes most of the assets and utils required
by the test suite: custom QUnit asserts, test helpers, lazy loaded assets, etc.

- another asset bundle, `web.qunit_suite_tests`_, contains all the test scripts.
This is typically where the test files are added to the suite.

- there is a `controller`_ in web, mapped to the route */web/tests*. This controller
simply renders the *web.qunit_suite* template.

- to execute the tests, one can simply point its browser to the route */web/tests*.
In that case, the browser will download all assets, and QUnit will take over.

- there is some code in `qunit_config.js`_ which logs in the console some
information when a test passes or fails.

- we want the runbot to also run these tests, so there is a test (in `test_js.py`_)
which simply spawns a browser and points it to the *web/tests* url. Note that
the browser_js method spawns a Chrome headless instance.


Modularity and testing
----------------------

With the way Odoo is designed, any addon can modify the behaviour of other parts
of the system. For example, the *voip* addon can modify the *FieldPhone* widget
to use extra features. This is not really good from the perspective of the
testing system, since this means that a test in the addon web will fail whenever
the voip addon is installed (note that the runbot runs the tests with all addons
installed).

At the same time, our testing system is good, because it can detect whenever
another module breaks some core functionality. There is no complete solution to
this issue. For now, we solve this on a case by case basis.

Usually, it is not a good idea to modify some other behaviour. For our voip
example, it is certainly cleaner to add a new *FieldVOIPPhone* widget and
modify the few views that needs it. This way, the *FieldPhone* widget is not
impacted, and both can be tested.

Adding a new test case
----------------------

Let us assume that we are maintaining an addon *my_addon*, and that we
want to add a test for some javascript code (for example, some utility function
myFunction, located in *my_addon.utils*). The process to add a new test case is
the following:

1. create a new file *my_addon/static/tests/utils_tests.js*. This file contains the basic code to
add a QUnit module *my_addon > utils*.

.. code-block:: javascript

odoo.define('my_addon.utils_tests', function (require) {
"use strict";

var utils = require('my_addon.utils');

QUnit.module('my_addon', {}, function () {

QUnit.module('utils');

});
});


2. In *my_addon/assets.xml*, add the file to the main test assets:

.. code-block:: xml

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="qunit_suite_tests" name="my addon tests" inherit_id="web.qunit_suite_tests">
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/my_addon/static/tests/utils_tests.js"/>
</xpath>
</template>
</odoo>

3. Restart the server and update *my_addon*, or do it from the interface (to
make sure the new test file is loaded)

4. Add a test case after the definition of the *utils* sub test suite:

.. code-block:: javascript

QUnit.test("some test case that we want to test", function (assert) {
assert.expect(1);

var result = utils.myFunction(someArgument);
assert.strictEqual(result, expectedResult);
});

5. Visit */web/tests/* to make sure the test is executed

Helper functions and specialized assertions
-------------------------------------------

Without help, it is quite difficult to test some parts of Odoo. In particular,
views are tricky, because they communicate with the server and may perform many
rpcs, which needs to be mocked. This is why we developed some specialized
helper functions, located in `test_utils.js`_.

- Mock test functions: these functions help setting up a test environment. The
most important use case is mocking the answers given by the Odoo server. These
functions use a `mock server`_. This is a javascript class that simulates
answers to the most common model methods: read, search_read, nameget, ...

- DOM helpers: useful to simulate events/actions on some specific target. For
example, testUtils.dom.click performs a click on a target. Note that it is
safer than doing it manually, because it also checks that the target exists,
and is visible.

- create helpers: they are probably the most important functions exported by
`test_utils.js`_. These helpers are useful to create a widget, with a mock
environment, and a lot of small detail to simulate as much as possible the
real conditions. The most important is certainly `createView`_.

- `qunit assertions`_: QUnit can be extended with specialized assertions. For
Odoo, we frequently test some DOM properties. This is why we made some
assertions to help with that. For example, the *containsOnce* assertion takes
a widget/jQuery/HtmlElement and a selector, then checks if the target contains
exactly one match for the css selector.

For example, with these helpers, here is what a simple form test could look like:

.. code-block:: javascript

QUnit.test('simple group rendering', function (assert) {
assert.expect(1);

var form = testUtils.createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</form>',
res_id: 1,
});

assert.containsOnce(form, 'table.o_inner_group');

form.destroy();
});

Notice the use of the testUtils.createView helper and of the containsOnce
assertion. Also, the form controller was properly destroyed at the end of
the test.

Best Practices
--------------

In no particular order:

- all test files should be added in *some_addon/static/tests/*
- for bug fixes, make sure that the test fails without the bug fix, and passes
with it. This ensures that it actually works.
- try to have the minimal amount of code necessary for the test to work.
- usually, two small tests are better than one large test. A smaller test is
easier to understand and to fix.
- always cleanup after a test. For example, if your test instantiates a widget,
it should destroy it at the end.
- no need to have full and complete code coverage. But adding a few tests helps
a lot: it makes sure that your code is not completely broken, and whenever a
bug is fixed, it is really much easier to add a test to an existing test suite.
- if you want to check some negative assertion (for example, that a HtmlElement
does not have a specific css class), then try to add the positive assertion in
the same test (for example, by doing an action that changes the state). This
will help avoid the test to become dead in the future (for example, if the css
class is changed).

Tips
----

- running only one test: you can (temporarily!) change the *QUnit.test(...)*
definition into *QUnit.only(...)*. This is useful to make sure that QUnit
only runs this specific test.
- debug flag: most create utility functions have a debug mode (activated by the
debug: true parameter). In that case, the target widget will be put in the DOM
instead of the hidden qunit specific fixture, and more information will be
logged. For example, all mocked network communications will be available in the
console.
- when working on a failing test, it is common to add the debug flag, then
comment the end of the test (in particular, the destroy call). With this, it
is possible to see the state of the widget directly, and even better, to
manipulate the widget by clicking/interacting with it.
- :doc:`Mock server <../frontend/unit_testing/mock_server>`

.. _reference/testing/integration-testing:

Expand Down Expand Up @@ -982,8 +747,6 @@ you can use the :meth:`~odoo.tests.BaseCase.assertQueryCount` method, integrated
.. _qunit: https://qunitjs.com/
.. _qunit_config.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_config.js#L49
.. _web.tests_assets: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L594
.. _web.qunit_suite: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L660
.. _web.qunit_suite_tests: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L680
.. _controller: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/controllers/main.py#L637
.. _test_js.py: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/tests/test_js.py#L13
.. _test_utils.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils.js
Expand Down
1 change: 1 addition & 0 deletions content/developer/reference/frontend.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ Web framework
frontend/mobile
frontend/qweb
frontend/odoo_editor
frontend/unit_testing
9 changes: 3 additions & 6 deletions content/developer/reference/frontend/assets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ like this:
'web/static/src/js/webclient.js',
'web/static/src/xml/webclient.xml',
],
'web.qunit_suite_tests': [
'web/static/src/js/webclient_tests.js',
'web.assets_unit_tests': [
'web/static/src/js/webclient.test.js',
],
},

Expand All @@ -94,10 +94,7 @@ know:
- `web.assets_frontend`: this bundle is about all that is specific to the public
website: ecommerce, portal, forum, blog, ...

- `web.qunit_suite_tests`: all javascript qunit testing code (tests, helpers, mocks)

- `web.qunit_mobile_suite_tests`: mobile specific qunit testing code

- `web.assets_unit_tests`: all javascript unit testing code (tests, helpers, mocks)

Operations
----------
Expand Down
90 changes: 90 additions & 0 deletions content/developer/reference/frontend/unit_testing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
:show-content:
:show-toc:

=======================
JavaScript Unit Testing
=======================

Writing unit tests is as important as writing the code itself: it helps to
ensure that the code is written according to a given specification and that it
remains correct as it evolves.

Testing Framework
=================

Testing the code starts with a testing framework. The framework provides a level
of abstraction that makes it possible to write tests in an easy and efficient way.
It also provides a set of tools to run the tests, make assertions and report the
results.

Odoo developers use a home-grown testing framework called :abbr:`HOOT (Hierarchically Organized
Odoo Tests)`. The main reason for using a custom framework is that it allows us to extend it based
on our needs (tags system, mocking of global objects, etc.).

On top of that framework we have built a set of tools to help us write tests for the web client
(`web_test_helpers`), and a mock server to simulate the server side (`mock_server`).

You can find links to the reference of each of these parts below, as well as a section filled with
examples and best practices for writing tests.

Setup
=====

Before learning how to write tests, it is good to start with the basics. The following steps
will ensure that your test files are properly picked up by the test runner.

Note that in existing addons, most of these steps can be skipped since the proper
folder structure and asset bundles are probably set up.

#. Writing files in the right **place**:

All JavaScript test files should be put under the `/static/tests` folder of the
related addon (e.g. :file:`/web/static/tests/env.test.js`).

#. Using the right **name**:

Test files must end with :file:`.test.js`. This is not only a convention, but a requirement
for test files to be picked up by the runner. All other JavaScript files will be
interpreted either as production code (i.e. the code to be tested), or as test
helper files (such as `web_test_helpers <{GITHUB_PATH}/addons/web/static/tests/web_test_helpers.js>`_).

.. note::
It is to be noted that there is an exception for :file:`.hoot.js` files, which are not
considered as test files, but as global modules for the whole test run, while other
JavaScript modules are re-created for each test suite. Since the same instance of
these modules will be running for the whole test run, they follow strict constraints,
such as restricted imports, or advanced memory management techniques to
ensure no side-effects are affecting tests.

#. Calling the files in the right **bundle**:

Test files, added in the right folder, must be included in the `web.assets_unit_tests`
bundle. For ease of use, this can be done with glob syntax to import all test
and test helper files:

.. code:: python

# Unit test files
'web.assets_unit_tests': [
'my_addon/static/tests/**/*',
],

#. Heading to the right **URL**:

To run tests, you can then go to the `/web/tests` URL.

.. tip::
This page can be accessed through :icon:`fa-bug` :menuselection:`Debug menu --> Run Unit Tests`.

Writing tests
=============

After creating and including test files, it is time to write tests. You may refer
to the following documentation sections to learn about the testing framework.

.. toctree::
:titlesonly:

unit_testing/hoot
unit_testing/web_helpers
unit_testing/mock_server
Loading