Тестування Odoo

Є багато способів перевірити додаток. В Odoo ми маємо три види тестів

  • Модульні тести Python (див. Тестування коду Python): корисні для тестування бізнес-логіки моделі

  • Модульні тести JS (див. Тестування коду JS): корисні для ізольованого тестування коду javascript

  • Тури (див. Інтеграційне тестування): тури імітують реальну ситуацію. Вони гарантують, що частини python і javascript належним чином спілкуються одна з одною.

Тестування коду Python

Odoo provides support for testing modules using Python’s unittest library.

Щоб написати тести, просто визначте підпакет tests у своєму модулі, він автоматично перевірятиметься на тестові модулі. Тестові модулі повинні мати назву, яка починатиметься з test_, і їх слід імпортувати з tests/__init__.py, напр.

your_module
├── ...
├── tests
|   ├── __init__.py
|   ├── test_bar.py
|   └── test_foo.py

і __init__.py містить:

from . import test_foo, test_bar

Попередження

тестові модулі, які не імпортовані з tests/__init__.py, не запускатимуться

Програма тестування просто запустить будь-який тестовий приклад, як описано в офіційній документації unittest, але Odoo надає ряд утиліт і помічників, пов’язаних з тестуванням вмісту Odoo (головним чином модулів):

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

Test class in which all test methods are run in a single transaction, but each test method is run in a sub-transaction managed by a savepoint. The transaction’s cursor is always closed without committing.

The data setup common to all methods should be done in the class method setUpClass, so that it is done once for all test methods. This is useful for test cases containing fast tests but with significant database setup common to all cases (complex in-db test data).

After being run, each test method cleans up the record cache and the registry cache. However, there is no cleanup of the registry models and fields. If a test modifies the registry (custom models and/or fields), it should prepare the necessary cleanup (self.registry.reset_changes()).

browse_ref(xid)[source]

Повертає об’єкт запису для наданого зовнішнього ідентифікатора

Параметри

xid – повний зовнішній ідентифікатор у формі module.identifier

Raise

ValueError, якщо не знайдено

Повертає

BaseModel

ref(xid)[source]

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

Параметри

xid – повний зовнішній ідентифікатор у формі module.identifier

Raise

ValueError, якщо не знайдено

Повертає

зареєстрований id

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

TestCase, у якому всі тестові методи виконуються в одній транзакції, транзакція починається з першого тестового методу та відкочується в кінці останнього.

browse_ref(xid)[source]

Повертає об’єкт запису для наданого зовнішнього ідентифікатора

Параметри

xid – повний зовнішній ідентифікатор у формі module.identifier

Raise

ValueError, якщо не знайдено

Повертає

BaseModel

ref(xid)[source]

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

Параметри

xid – повний зовнішній ідентифікатор у формі module.identifier

Raise

ValueError, якщо не знайдено

Повертає

зареєстрований id

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

Транзакційний HTTP TestCase з url_open і безголовими помічниками Chrome.

browser_js(url_path, code, ready='', login=None, timeout=60, cookies=None, error_checker=None, watch=False, cpu_throttling=None, **kw)[source]

Test JavaScript code running in the browser.

To signal success test do: console.log('test successful') To signal test failure raise an exception or call console.error with a message. Test will stop when a failure occurs if error_checker is not defined or returns True for this message

Параметри
  • url_path (string) – URL path to load the browser page on

  • code (string) – JavaScript code to be executed

  • ready (string) – JavaScript object to wait for before proceeding with the test

  • login (string) – logged in user which will execute the test. e.g. „admin“, „demo“

  • timeout (int) – maximum time to wait for the test to complete (in seconds). Default is 60 seconds

  • cookies (dict) – dictionary of cookies to set before loading the page

  • error_checker – function to filter failures out. If provided, the function is called with the error log message, and if it returns False the log is ignored and the test continue If not provided, every error log triggers a failure

  • watch (bool) – open a new browser window to watch the test execution

  • cpu_throttling (int) – CPU throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc)

browse_ref(xid)[source]

Повертає об’єкт запису для наданого зовнішнього ідентифікатора

Параметри

xid – повний зовнішній ідентифікатор у формі module.identifier

Raise

ValueError, якщо не знайдено

Повертає

BaseModel

ref(xid)[source]

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

Параметри

xid – повний зовнішній ідентифікатор у формі module.identifier

Raise

ValueError, якщо не знайдено

Повертає

зареєстрований id

odoo.tests.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 „at_install“.

When using class inheritance, the tags ARE inherited.

За замовчуванням тести запускаються один раз відразу після встановлення відповідного модуля. Тестові приклади також можна налаштувати для запуску після встановлення всіх модулів, а не для запуску одразу після встановлення модуля:

# coding: utf-8
from odoo.tests import HttpCase, tagged

# This test should only be executed after all modules have been installed.
@tagged('-at_install', 'post_install')
class WebsiteVisitorTests(HttpCase):
  def test_create_visitor_on_tracked_page(self):
      Page = self.env['website.page']

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

class TestModelA(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...

Примітка

Методи тестування мають починатися з test_

class odoo.tests.Form(record, view=None)[source]

Реалізація представлення форми на стороні сервера (частково)

Реалізує більшу частину потоку маніпуляцій «представлення форми», щоб тести на стороні сервера могли більш правильно відображати поведінку, яка спостерігалася б під час маніпулювання інтерфейсом:

  • call the relevant onchanges on «creation»;

  • call the relevant onchanges on setting fields;

  • properly handle defaults & onchanges around x2many fields.

Saving the form returns the current record (which means the created record if in creation mode). It can also be accessed as form.record, but only when the form has no pending changes.

Regular fields can just be assigned directly to the form. In the case of Many2one fields, one can assign a recordset:

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

One can also use the form as a context manager to create or edit a record. The changes are automatically saved at the end of the scope:

with Form(self.env['sale.order']) as f1:
    f1.partner_id = a_partner
    # f1 is saved here

# retrieve the created record
so = f1.record

# call Form on record => edition mode
with Form(so) as f2:
    f2.payment_term_id = env.ref('account.account_payment_term_15days')
    # f2 is saved here

Для полів Many2many саме поле є M2MProxy і може бути змінено додаванням або видаленням записів:

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:
    f.partner_id = a_partner
    # 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
Параметри
  • record (odoo.models.Model) – empty or singleton recordset. An empty recordset will put the view in «creation» mode from default values, while a singleton will put it in «edit» mode and only load the view’s data.

  • view (int | str | odoo.model.Model) – id, xmlid або фактичний об’єкт перегляду для використання для змін і обмежень представлення. Якщо жодного не надано, просто завантажується типове представлення для моделі.

Нове в версії 12.0.

save()[source]

Save the form (if necessary) and return the current record:

  • 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.

When nothing must be saved, it simply returns the current record.

Викликає

AssertionError – якщо у формі є незаповнені обов’язкові поля

property record

Return the record being edited by the form. This attribute is readonly and can only be accessed when the form has no pending changes.

class odoo.tests.M2MProxy(form, field_name)[source]

Proxy object for editing the value of a many2many field.

Поводиться як Sequence наборів записів, які можна індексувати або нарізати, щоб отримати фактичні основні набори записів.

add(record)[source]

Додає record до поля, запис уже має існувати.

Додавання буде завершено лише після збереження батьківського запису.

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

Видаляє запис із певним індексом або з наданим ідентифікатором із поля.

clear()[source]

Видаляє всі існуючі записи в m2m

class odoo.tests.O2MProxy(form, field_name)[source]

Proxy object for editing the value of a one2many field.

new()[source]

Повертає Форму для нового запису One2many, правильно ініціалізованого.

Форма створюється з представлення списку, якщо його можна редагувати, або з представлення форми поля в іншому випадку.

Викликає

AssertionError – якщо поле не редагується

edit(index)[source]

Повертає Форму для редагування вже існуючого запису One2many.

Форма створюється з представлення списку, якщо його можна редагувати, або з представлення форми поля в іншому випадку.

Викликає

AssertionError – якщо поле не редагується

remove(index)[source]

Видаляє запис в index з батьківської форми.

Викликає

AssertionError – якщо поле не редагується

Запуск тестів

Тести запускаються автоматично під час встановлення або оновлення модулів, якщо --test-enable було ввімкнено під час запуску сервера Odoo.

Вибір тесту

В Odoo тести Python можна позначати тегами, щоб полегшити вибір тестів під час виконання тестів.

Subclasses of odoo.tests.BaseCase (usually through TransactionCase or HttpCase) are automatically tagged with standard and at_install by default.

Виклик

--test-tags можна використовувати для вибору/фільтрування тестів для запуску в командному рядку. Це передбачає --test-enable, тому немає необхідності вказувати --test-enable, коли використовуючи --test-tags.

Цей параметр за замовчуванням має значення +standard, що означає, що тести з тегом standard (явно чи неявно) виконуватимуться за замовчуванням під час запуску Odoo за допомогою --test-enable.

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

Аргументами декоратора є назви тегів у вигляді рядків.

Небезпека

tagged() is a class decorator, it has no effect on functions or methods

Щоб видалити теги замість того, щоб додавати або вибирати їх, наприклад. якщо ви не хочете, щоб ваш тест виконувався за замовчуванням, ви можете видалити тег standard:

from odoo.tests import TransactionCase, tagged

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

Цей тест не буде вибрано за замовчуванням, щоб запустити відповідний тег потрібно буде вибрати явно:

$ odoo-bin --test-tags nice

Зауважте, що виконуватимуться лише тести з тегом nice. Щоб запустити як ``both``, так і ``standard`` тести, надайте кілька значень :option:`–test-tags <odoo-bin –test-tags>`: у командному рядку значення є *additive (ви вибираєте всі тести з any із зазначених тегів)

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

Параметр config switch також приймає префікси + і -. Префікс + мається на увазі, тому він абсолютно необов’язковий. Префікс - (мінус) призначений для скасування вибору тестів, позначених префіксованими тегами, навіть якщо вони вибрані іншими вказаними тегами, наприклад. якщо є standard тести, які також позначені як slow, ви можете запустити всі стандартні тести except повільних:

$ odoo-bin --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 explicitly 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):
    ...

Окрім тегів, ви також можете вказати конкретні модулі, класи або функції для тестування. Повний синтаксис формату, який приймає --test-tags є:

[-][tag][/module][:class][.method]

Отже, якщо ви хочете протестувати модуль stock_account, ви можете використовувати:

$ odoo-bin --test-tags /stock_account

Якщо ви хочете протестувати певну функцію з унікальною назвою, його можна вказати безпосередньо:

$ odoo-bin --test-tags .test_supplier_invoice_forwarded_by_internal_user_without_supplier

Це еквівалентно

$ odoo-bin --test-tags /account:TestAccountIncomingSupplierInvoice.test_supplier_invoice_forwarded_by_internal_user_without_supplier

якщо назва тесту однозначна. Кілька модулів, класів і функцій можна вказати одночасно, розділивши їх символом ,, як у звичайних тегах.

Спеціальні теги

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

    Це означає, що тест без тегів виконуватиметься за умовчанням, коли тести ввімкнено.

  • at_install: означає, що тест буде виконано одразу після встановлення модуля та перед встановленням інших модулів. Це неявний тег за умовчанням.

  • post_install: означає, що тест буде виконано після встановлення всіх модулів. Це те, що вам потрібно для тестів HttpCase більшість часу.

    Зауважте, що це не винятково для at_install, однак, оскільки ви, як правило, не бажаєте, щоб обидва post_install зазвичай поєднувалися з -at_install під час позначення тестового класу.

Приклади

Важливо

Тести будуть виконуватися тільки в установлених модулях. Якщо ви починаєте з чистої бази даних, вам потрібно принаймні один раз встановити модулі з перемикачем -i. Після цього він більше не потрібен, якщо тільки вам не потрібно оновити модуль, у такому випадку можна використовувати -u. Для простоти ці перемикачі не вказані в наведених нижче прикладах.

Запустіть лише тести з модуля продажу:

$ odoo-bin --test-tags /sale

Запустіть тести з модуля продажу, але не ті, які позначені як повільні:

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

Виконуйте лише ті тести, які є на складі або позначені як повільні:

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

Примітка

-standard неявний (не обов’язковий) і представлений для ясності

Тестування коду JS

Тестування складної системи є важливим запобіжним заходом для запобігання регресії та гарантії, що деякі базові функції все ще працюють. Оскільки Odoo має нетривіальну кодову базу в Javascript, її необхідно перевірити. У цьому розділі ми обговоримо практику тестування JS-коду окремо: ці тести залишаються в браузері й не повинні досягати сервера.

Набір тестів Qunit

Фреймворк Odoo використовує фреймворк для тестування бібліотеки QUnit як засіб виконання тестів. QUnit визначає поняття tests і modules (набору пов’язаних тестів) і надає нам веб-інтерфейс для виконання тестів.

Наприклад, ось як може виглядати тест pyUtils:

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");
});

Основний спосіб запустити набір тестів - мати запущений сервер Odoo, а потім перейти у веб-браузері до /web/tests. Потім набір тестів буде виконано механізмом Javascript веб-браузера.

../../../_images/tests.png

Веб-інтерфейс користувача має багато корисних функцій: він може запускати лише деякі підмодулі або фільтрувати тести, які відповідають рядку. Він може показати всі твердження, невдалі чи пройдені, повторно запустити певні тести, …

Попередження

Під час роботи набору тестів переконайтеся, що:

  • вікно браузера сфокусовано,

  • він не збільшений/зменшений. Він повинен мати рівно 100% масштабу.

Якщо це не так, деякі тести будуть невдалими без належного пояснення.

Тестування інфраструктури

Ось короткий огляд найважливіших частин інфраструктури тестування:

  • є набір ресурсів під назвою web.qunit_suite. Цей набір містить основний код (загальні активи + бекенд активів), деякі бібліотеки, засіб виконання тестів QUnit і тестові пакети, перелічені нижче.

  • пакет під назвою web.tests_assets включає більшість активів і утиліт, необхідних для набору тестів: користувальницькі підтвердження QUnit, помічники тестування, активи з відкладеним завантаженням тощо.

  • інший комплект активів, web.qunit_suite_tests, містить усі тестові сценарії. Зазвичай це місце, де тестові файли додаються до набору.

  • у web є controller, зіставлений з маршрутом /web/tests. Цей контролер просто рендерить шаблон web.qunit_suite.

  • щоб виконати тести, можна просто вказати браузер на маршрут /web/tests. У цьому випадку браузер завантажить усі ресурси, а QUnit візьме на себе роботу.

  • є певний код у qunit_config.js, який записує в консоль певну інформацію, коли тест проходить чи не проходить.

  • ми хочемо, щоб runbot також запускав ці тести, тому існує тест (у test_js.py), який просто породжує браузер і спрямовує його на URL-адресу web/tests. Зауважте, що метод browser_js створює безголовий екземпляр Chrome.

Модульність і тестування

Завдяки тому, як Odoo розроблено, будь-який аддон може змінювати поведінку інших частин системи. Наприклад, аддон voip може змінити віджет FieldPhone для використання додаткових функцій. Це не дуже добре з точки зору системи тестування, оскільки це означає, що тест у веб-доповненні буде невдалим щоразу, коли встановлено доповнення VoIP (зверніть увагу, що Runbot запускає тести з усіма встановленими додатками).

У той же час наша система тестування хороша, оскільки вона може виявити, коли інший модуль порушує певну основну функціональність. Повного вирішення цієї проблеми немає. Наразі ми вирішуємо це в кожному окремому випадку.

Зазвичай не варто змінювати якусь іншу поведінку. Для нашого прикладу VoIP, безперечно, чистіше додати новий віджет FieldVOIPPhone і змінити декілька представлень, які його потребують. Таким чином, віджет FieldPhone не зазнає впливу, і обидва можна перевірити.

Додавання нового тесту

Припустімо, що ми підтримуємо аддон my_addon і хочемо додати тест для деякого коду javascript (наприклад, якусь службову функцію myFunction, розташовану в my_addon.utils). Процес додавання нового тестового прикладу такий:

  1. створити новий файл my_addon/static/tests/utils_tests.js. Цей файл містить базовий код для додавання модуля QUnit my_addon > utils.

    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. У my_addon/assets.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. Перезапустіть сервер і оновіть my_addon або зробіть це з інтерфейсу (щоб переконатися, що новий тестовий файл завантажено)

  4. Додайте тестовий приклад після визначення набору підтестів utils:

    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. Відвідайте /web/tests/, щоб переконатися, що тест виконано

Допоміжні функції та спеціальні твердження

Без сторонньої допомоги досить важко протестувати деякі частини Odoo. Зокрема, представлення є складними, оскільки вони взаємодіють із сервером і можуть виконувати багато rpc, які потрібно імітувати. Ось чому ми розробили кілька спеціалізованих допоміжних функцій, розташованих у test_utils.js.

  • Функції макетного тестування: ці функції допомагають налаштувати тестове середовище. Найважливішим випадком використання є висміювання відповідей, наданих сервером Odoo. Ці функції використовують макетний сервер. Це клас javascript, який симулює відповіді на найпоширеніші методи моделі: read, search_read, nameget, …

  • Помічники DOM: корисні для симуляції подій/дій щодо певної цілі. Наприклад, testUtils.dom.click виконує клацання по цілі. Зауважте, що це безпечніше, ніж робити це вручну, оскільки воно також перевіряє, чи ціль існує та чи є видимою.

  • створити помічники: це, ймовірно, найважливіші функції, експортовані test_utils.js. Ці помічники корисні для створення віджетів із макетом середовища та великою кількістю дрібних деталей, щоб якомога більше імітувати реальні умови. Найважливішим, безумовно, є createView.

  • qunit assertions: QUnit можна розширити за допомогою спеціальних тверджень. Для Odoo ми часто тестуємо деякі властивості DOM. Ось чому ми зробили деякі твердження, щоб допомогти з цим. Наприклад, твердження containsOnce приймає widget/jQuery/HtmlElement і селектор, а потім перевіряє, чи ціль містить точно один збіг для селектора css.

Наприклад, за допомогою цих помічників ось як може виглядати простий тест форми:

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();
});

Зверніть увагу на використання помічника testUtils.createView та твердження containsOnce. Крім того, контролер форми було належним чином знищено в кінці тесту.

Кращі практики

Без особливого порядку:

  • усі тестові файли слід додати в some_addon/static/tests/

  • для виправлень помилок переконайтеся, що тест не вдається без виправлення помилок і проходить разом із ним. Це гарантує, що він дійсно працює.

  • намагайтеся мати мінімальну кількість коду, необхідного для роботи тесту.

  • зазвичай два невеликих тести краще, ніж один великий. Менший тест легше зрозуміти та виправити.

  • завжди прибирання після тесту. Наприклад, якщо ваш тест створює екземпляр віджета, він має знищити його в кінці.

  • немає необхідності мати завершене і повне покриття коду. Але додавання кількох тестів дуже допомагає: це гарантує, що ваш код не зламано повністю, і щоразу, коли помилку виправлено, набагато простіше додати тест до існуючого набору тестів.

  • якщо ви хочете перевірити якесь негативне твердження (наприклад, що HtmlElement не має певного класу css), тоді спробуйте додати позитивне твердження в той самий тест (наприклад, виконавши дію, яка змінює стан). Це допоможе уникнути знищення тесту в майбутньому (наприклад, якщо буде змінено клас css).

Поради

  • запуск лише одного тесту: ви можете (тимчасово!) змінити визначення QUnit.test(…) на QUnit.only(…). Це корисно, щоб переконатися, що QUnit виконує лише цей конкретний тест.

  • прапор налагодження: більшість функцій утиліти create мають режим налагодження (активується параметром debug: true). У цьому випадку цільовий віджет буде розміщено в DOM замість прихованого специфічного фікстура qunit, і більше інформації буде зареєстровано. Наприклад, усі імітовані мережеві комунікації будуть доступні в консолі.

  • під час роботи над невдалим тестом зазвичай додають позначку налагодження, а потім коментують кінець тесту (зокрема, виклик знищення). Завдяки цьому можна безпосередньо бачити стан віджета, а ще краще – маніпулювати віджетом, натискаючи/взаємодіючи з ним.

Інтеграційне тестування

Тестування коду Python і коду JS окремо дуже корисно, але це не доводить, що веб-клієнт і сервер працюють разом. Щоб зробити це, ми можемо написати інший тип тесту: тури. Тур – це міні-сценарій якогось цікавого ділового потоку. Він пояснює послідовність кроків, яких слід виконати. Потім виконавець тесту створить браузер PhantomJs, направить його на відповідну URL-адресу та імітує клацання та введення відповідно до сценарію.

Написання тестового туру

Структура

Щоб написати тестовий тур для your_module, почніть зі створення необхідних файлів:

your_module
├── ...
├── static
|   └── tests
|       └── tours
|           └── your_tour.js
├── tests
|   ├── __init__.py
|   └── test_calling_the_tour.py
└── __manifest__.py

Тоді ви можете:

  • update __manifest__.py to add your_tour.js in the assets.

    'assets': {
        'web.assets_tests': [
            'your_module/static/tests/tours/your_tour.js',
        ],
    },
    
  • оновіть __init__.py в папці tests, щоб імпортувати test_calling_the_tour.

Перегляньте також

Javascript

  1. Налаштуйте свій тур, зареєструвавши його.

    /** @odoo-module **/
    import tour from 'web_tour.tour';
    tour.register('rental_product_configurator_tour', {
        url: '/web',  // Here, you can specify any other starting url
        test: true,
    }, [
        // Your sequence of steps
    ]);
    
  2. Додайте будь-який крок, який хочете.

Every step contains at least a trigger. You can either use the predefined steps or write your own personalized step.

Ось кілька прикладів кроків:

Example

// First step
tour.stepUtils.showAppsMenuItem(),
// Second step
{
    trigger: '.o_app[data-menu-xmlid="your_module.maybe_your_module_menu_root"]',
    edition: 'community'  // Optional
}, {
    // Third step
},

Example

{
    trigger: '.js_product:has(strong:contains(Chair floor protection)) .js_add',
    extra_trigger: '.oe_advanced_configurator_modal',  // This ensure we are in the wizard
},

Example

{
    trigger: 'a:contains("Add a product")',
    // Extra-trigger to make sure a line is added before trying to add another one
    extra_trigger: '.o_field_many2one[name="product_template_id"] .o_external_button',
},

Ось кілька можливих аргументів для ваших індивідуальних кроків:

  • trigger: Selector/element to run an action on. The tour will wait until the element exists and is visible before run-ing the action on it.

  • extra_trigger: Optional secondary condition for the step to run. Will be waited for like the trigger element but the action will not run on the extra trigger.

    Useful to have a precondition, or two different and unrelated conditions.

  • run: Action to perform on the trigger element.

    By default, tries to set the trigger’s content to Text if it’s an input, otherwise click it.

    The action can also be:

    • A function, synchronous, executed with the trigger’s Tip as context (this) and the action helpers as parameter.

    • The name of one of the action helpers, which will be run on the trigger element:

      click

      Clicks the element, performing all the relevant intermediate events.

      text content

      Clicks (focuses) the element then sets content as the element’s value (if an input), option (if a select), or content.

      dblclick, tripleclick

      Same as click with multiple repetitions.

      clicknoleave

      By default, click (and variants) will trigger «exit» events on the trigger element (mouseout, mouseleave). This helper suppresses those (note: further clicks on other elements will not trigger those events implicitly).

      text_blur

      Similar to text but follows the edition with focusout and blur events.

      drag_and_drop target

      Simulates the dragging of the trigger element over to the target.

  • edition: Optional,

    • Якщо ви не вкажете випуск, крок буде активним і в community, і в enterprise.

    • Іноді на enterprise чи в community крок буде іншим. Потім ви можете написати два кроки: один для enterprise версії та інший для community.

    • Як правило, ви хочете вказати випуск для кроків, які використовують головне меню, оскільки головні меню відрізняються в community та на enterprise.

  • position: Optional, "top", "right", "bottom", or "left". Where to position the tooltip relative to the target when running interactive tours.

  • content: Optional but recommended, the content of the tooltip in interactive tours, also logged to the console so very useful to trace and debug automated tours.

  • auto: Whether the tour manager should wait for the user to perform the action if the tour is interactive, defaults to false.

  • in_modal: If set the trigger element will be searched only in the top modal window, defaults to false.

  • timeout: How long to wait until the step can run, in milliseconds, 10000 (10 seconds).

Важливо

The last step(s) of a tour should always return the client to a «stable» state (e.g. no ongoing editions) and ensure all side-effects (network requests) have finished running to avoid race conditions or errors during teardown.

Перегляньте також

Python

To start a tour from a python test, make the class inherit from HTTPCase, and call start_tour:

def test_your_test(self):
    # Optional Setup
    self.start_tour("/web", 'your_module.your_tour_name', login="admin")
    # Optional verifications

Поради щодо налагодження

Observing tours in a browser

There are two ways with different tradeoffs:

watch=True

When running a tour locally via the test suite, the watch=True parameter can be added to the browser_js or start_tour call:

self.start_tour("/web", code, watch=True)

This will automatically open a Chrome window with the tour being run inside it.

Advantages
  • always works if the tour has Python setup / surrounding code, or multiple steps

  • runs entirely automatically (just select the test which launches the tour)

  • transactional (should always be runnable multiple times)

Drawbacks
  • only works locally

  • only works if the test / tour can run correctly locally

Run via browser

Tours can also be launched via the browser UI, either by calling

odoo.startTour(tour_name);

in the javascript console, or by enabling tests mode by setting ?debug=tests in the URL, then selecting Start Tour in the debug menu and picking a tour:

../../../_images/tours.png
Advantages
  • easier to run

  • can be used on production or test sites, not just local instances

  • allows running in «Onboarding» mode (manual steps)

Drawbacks
  • harder to use with test tours involving Python setup

  • may not work multiple times depending on tour side-effects

Порада

It’s possible to use this method to observe or interact with tours which require Python setup:

  • add a python breakpoint before the relevant tour is started (start_tour or browser_js call)

  • when the breakpoint is hit, open the instance in your browser

  • run the tour

At this point the Python setup will be visible to the browser, and the tour will be able to run.

You may want to comment the start_tour or browser_js call if you also want the test to continue afterwards, depending on the tour’s side-effects.

Скріншоти та скрінкасти під час тестування browser_js

When running tests that use HttpCase.browser_js from the command line, the Chrome browser is used in headless mode. By default, if a test fails, a PNG screenshot is taken at the moment of the failure and written in

'/tmp/odoo_tests/{db_name}/screenshots/'

Починаючи з Odoo 13.0, було додано два нові аргументи командного рядка для керування цією поведінкою: --screenshots і --screencasts

Introspecting / debugging steps

When trying to fix / debug a tour, the screenshots (on failure) are not necessarily sufficient. In that case it can be useful to see what’s happening at some or each step.

While this is pretty easy when in an «onboarding» (as they’re mostly driven explicitly by the user) it’s more complicated when running «test» tours, or when running tours through the test suite. In that case there are two main tricks:

  • Have a step with a run() { debugger; } action.

    This can be added to an existing step, or can be a new dedicated step. Once the step’s trigger is matched, the execution will stop all javascript execution.

    Advantages
    • very simple

    • the tour restarts as soon as you resume execution

    Drawbacks
    • page interaction is limited as all javascript is blocked

    • debugging the inside of the tour manager is not very useful

  • Add a step with a trigger which never succeeds and a very long timeout.

    The browser will wait for the trigger until the timeout before it fails the tour, this allows inspecting and interacting with the page until the developer is ready to resume, by manually enabling the trigger (a nonsense class is useful there, as it can be triggered by adding the class to any visible element of the page).

    Advantages
    • allows interacting with the page

    • easy to apply to a step which times out (just add a long timeout then look around)

    • no useless (for this situation) debugger UI

    Drawbacks
    • more manual, especially when resuming

Тестування продуктивності

Кількість запитів

One of the ways to test performance is to measure database queries. Manually, this can be tested with the --log-sql CLI parameter. If you want to establish the maximum number of queries for an operation, you can use the assertQueryCount() method, integrated in Odoo test classes.

with self.assertQueryCount(11):
    do_something()