Тестирование в Odoo

Существует множество способов протестировать приложение. В Odoo у мы используем три вида тестов

  • Python юнит тесты(see Тестирование Python): полезно для тестирования бизнес логики моделей
  • JS юнит тесты (смотрите Тестирование JS): полезно для тестирования javascript кода в изолированном окружении
  • Туры (смотрите`Интеграционное тестирование`_): туры имитируют реальную ситуацию. Они гарантируют, что части python и javascript правильно общаются друг с другом.

Тестирование кода Python

Odoo обеспечивает поддержку тестирования модулей с использованием unittest.

Чтобы написать тесты, просто определите субмодуль``tests`` в вашем модуле, он будет автоматически запущен для тестирования модулей. Модули тестирования должны иметь имя, начинающееся с test_, и должны быть импортированы из tests/__init__.py, например:

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

а __init__.py содержит:

from . import test_foo, test_bar

При тестировании будет запускаться любой тест, как описано в официальной unittest documentation, но Odoo предоставляет ряд утилит и помощников, связанных с тестированием контента Odoo (главным образом модулей):

class odoo.tests.common.TransactionCase(methodName='runTest')[исходный код]

TestCase, в котором каждый тестовый метод запускается в своей транзакции и с собственным курсором. Откат транзакции и закрытие курсора после каждого теста.

browse_ref(xid)[исходный код]

Возвращает объект записи для предоставленного external identifier

Параметры
xid – Полный external identifier, в форме module.identifier
Raise
ValueError если не найден
Результат
ref(xid)[исходный код]

Возвращает ID базы данных для предоставленного external identifier, ярлык для get_object_reference

Параметры
xid – Полный external identifier, в форме module.identifier
Raise
ValueError если не найден
Результат
Зарегистрированный id
class odoo.tests.common.SingleTransactionCase(methodName='runTest')[исходный код]

TestCase, в которой все тестовые методы запускаются в одной транзакции, транзакция запускается с помощью первого метода тестирования и откатывается в конце последнего.

browse_ref(xid)[исходный код]

Возвращает объект записи для предоставленного external identifier

Параметры
xid – Полный external identifier, в форме module.identifier
Raise
ValueError если не найден
Результат
ref(xid)[исходный код]

Возвращает ID базы данных для предоставленного external identifier, ярлык для get_object_reference

Параметры
xid – Полный external identifier, в форме module.identifier
Raise
ValueError если не найден
Результат
Зарегистрированный id
class odoo.tests.common.SavepointCase(methodName='runTest')[исходный код]

Аналогичен SingleTransactionCase в том, что все методы тестирования выполняются в одной транзакции, но каждый TestCase выполняется в откатанной точке сохранения (под-транзакции).

Полезно для тестовых случаев, содержащих быстрые тесты, но со значительной настройкой базы данных, общей для всех случаев (сложные тестовые данные в бд): :meth:`~.setUpClass`может использоваться для генерации тестовых данных БД один раз и тогда все TestCase будут использовать одни и те же данные, не влияя друг на друга, и без необходимости заново создавать тестовые данные.

class odoo.tests.common.HttpCase(methodName='runTest')[исходный код]

Транзакционный HTTP TestCase с url_open и Chrome headless инструментами.

browse_ref(xid)[исходный код]

Возвращает объект записи для предоставленного external identifier

Параметры
xid – Полный external identifier, в форме module.identifier
Raise
ValueError если не найден
Результат
phantom_js(url_path, code, ready='', login=None, timeout=60, **kw)[исходный код]

Протируетjs код запустив его в браузере - дополнительно авторизуется в системе как login, загрузит объявленную страницу url_path, дождется пока объект сможет быть выполнен и исполнит code внутри страницы

Чтобы сообщить об успешном выполнении теста, выполните: console.log (test success). Чтобы сообщить о неудачном завершении теста, вызовите исключение или вызовите console.error

ref(xid)[исходный код]

Возвращает ID базы данных для предоставленного external identifier, ярлык для get_object_reference

Параметры
xid – Полный external identifier, в форме module.identifier
Raise
ValueError если не найден
Результат
Зарегистрированный id
odoo.tests.common.tagged(*tags)[исходный код]

Декоратор для тегирования объектов BaseCase, Теги хранятся в сете, доступ к которому можно получить из атрибута «test_tags». Тег с префиксом «-» удалит тег, например, удалить тег standard. По умолчанию все классы тестов из odoo.tests.common имеют атрибут test_tags, который по умолчанию равен standard, а также техническое имя модуля. При использовании наследования классов теги НЕ наследуются.

По умолчанию тесты запускаются сразу после установки соответствующего модуля. Но можно настроить, чтобы тестирование запускалось после установки всех модулей, а не после установки одного модуля:

odoo.tests.common.at_install(flag)[исходный код]

Устанавливает состояние at-install для теста, флаг является логическим значением, указывающим, должен ли тест (True) или не должен (False) выполняться во время установки модуля.

По умолчанию тесты запускаются сразу после установки модуля перед началом установки следующего модуля.

Не рекомендуется, начиная с версии 12.0: at_install еперь флаг, вы можете использовать tagged() чтобы добавить/удалить его, более того tagged работает только на классах тестов

odoo.tests.common.post_install(flag)[исходный код]

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

По умолчанию тесты не запускаются после установки всех модулей в текущем наборе для.

Не рекомендуется, начиная с версии 12.0: post_install теперь флаг, вы можете использовать tagged() для того, чтобы добавить/удалить его, а так же tagged работает только на классах тестов

Наиболее распространенная ситуация заключается в использовании TransactionCase и тестирования свойства модели в каждом методе:

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)[исходный код]

Реализация представления Form на стороне сервера (частично)

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

  • вызовите default_get и соответствующие изменения на «создание»
  • вызовите соответствующие onchanges к настройкам полей
  • правильно обработайте defaults и onchanges для x2many полей

Сохранение формы возвращает созданную запись в режиме «creation».

Регулярные поля могут быть просто назначены непосредственно в форме, для полей Many2one назначается одноэлементный набор записей

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

При редактировании записи используйте Form в качестве диспетчера контекста, чтобы автоматически сохранить ее в конце области:

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)

И наконец поле One2many это класс O2MProxy.

Поскольку класс One2many существует только через своего родителя, он более непосредственно управляется путем создания sub-forms с помощью new() и edit() методов. Обычно они используются в качестве менеджеров контекста, так как они сохраняются в родительской записи:

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
Параметры
  • recordp (odoo.models.Model) – пустой или одиночный набор записей. Пустой набор записей переведет представление в режим «creation» и вызовет вызовы default_get и on-load onchanges, синглтон переведет его в режим «редактирования» и загрузит только данные представления.
  • view (int | str | odoo.model.Model) – id, xmlid или фактический объект представления, чтобы использовать для onchanges и ограничений представления. Если ничего не указано, просто загружается представление по умолчанию для модели.

Добавлено в версии 12.0.

save()[исходный код]

Сохраняет форму, возвращает созданную запись, если применимо

  • не сохраняет поля readonly
  • не сохраняет неизмененные поля (во время редактирования) - любое присвоение или возвращение onchange помечает поле как измененное, даже если установлено его текущее значение
Исключение
AssertionError – если в форме есть незаполненное обязательное поле
class odoo.tests.common.M2MProxy[исходный код]

Ведет себя как Sequence наборов записей, может быть проиндексирован или к нему может быть применен метод slice для получения актуальных наборов записей.

add(record)[исходный код]

Добавляет record к полю, запись должна уже существовать.

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

clear()[исходный код]

Удаляет все существующие записи m2m

remove(id=None, index=None)[исходный код]

Удаляет запись с определенным индексом или с указанным идентификатором из поля.

class odoo.tests.common.O2MProxy[исходный код]
edit(index)[исходный код]

Возвращает Form для редактирования существующей записи One2many.

Форма создается из представления списка, если он доступен для редактирования, или в противном случае из формы поля.

Исключение
AssertionError – если поле не редактируемое
new()[исходный код]

Возвращает класс Form для новой One2many ,правильно инициализированной,записи.

Форма создается из представления списка, если он доступен для редактирования, или в противном случае из формы поля.

Исключение
AssertionError – если поле не редактируемое
remove(index)[исходный код]

Удаляет запись в index из родительской формы.

Исключение
AssertionError – если поле не редактируемое

Выполнение тестов

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

Выбор теста

В Odoo тесты Python могут быть помечены тегом для облегчения выбора тестов при их запуске.

Подклассы odoo.tests.common.BaseCase (обычно через TransactionCase, SavepointCase или HttpCase) автоматически помечаются как standard, at_install и именем их исходного модуля по умолчанию.

Введение

--test-tags может использоваться для выбора/фильтрации тестов для запуска с помощью командной строки.

Эта опция по умолчанию имеет значение +standard что означает, что тесты с тегом standard (явно или неявно) будут запускаться по умолчанию при запуске Odoo с параметром --test-enable.

При написании тестов декоратор tagged() можно использовать в классах тестов для добавления или удаления тегов.

Аргументы декоратора - это имена тегов в виде строк.

Тэги могут иметь префикс со знаком минус (-) чтобы удалить их вместо добавления, например. если вы не хотите, чтобы ваш тест выполнялся по умолчанию, вы можете удалить тег standard:

from odoo.tests import TransactionCase, tagged

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

Этот тест не будет выбран по умолчанию, т.к. для его запуска соответствующий тег должен быть выбран явно:

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

Обратите внимание, что будут выполняться только тесты с тегом nice. Для запуска обоих * ``nice`` и ``standard`` тестов укажите несколько значений для :option:`–test-tags <odoo-bin –test-tags>`: в командной строке, значения этого параметра являются *аддитивными (вы выбираете все тесты с любым из указанных тегов)

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

Параметр также принимает префиксы + `` и ``-. Префикс + `` подразумевается и, следовательно, его указывать не обязательно. Префикс ``- (минус) предназначен для отмены выбора тестов, помеченных префиксными тегами, даже если они выбраны другими указанными тегами, например, если есть standard тесты, которые также помечены как slow, вы можете запустить все стандартные тесты ,*за исключением* медленных:

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

Когда вы пишете тест, который не наследуется от BaseCase, этот тест не будет иметь тегов по умолчанию, вы должны добавить их явно, чтобы тест был включен в набор тестов по умолчанию , Это распространенная проблема при использовании простого unittest.TestCase, поскольку они не просто не запускаются:

import unittest
from odoo.tests import tagged

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

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

  • standard: Все тесты Odoo, которые наследуются от BaseCase неявно помечены как стандартные. --test-tags по умолчанию standard.

    Это означает, что тест без тегов будет выполняться по умолчанию, когда тесты включены.

  • at_install: Означает, что тест будет выполнен сразу после установки модуля и до установки других модулей. Это неявный тег по умолчанию.
  • post_install: Означает, что тест будет выполнен после установки всех модулей. Это то, что вы хотите для тестов HttpCase большую часть времени.

    Обратите внимание, что это такое поведение не эксклюзивно для at_install, однако, поскольку вы, как правило, не будете использовать оба параметра, при это post_install обычно сопряжен с -at_install при тегировании класса теста.

  • module_name: классы тестов Odoo расширяющие BaseCase еявно помечены тегом равным техническому имени их модуля. Это позволяет легко выбирать или исключать определенные модули при тестировании, например, если вы хотите запускать тесты только из модуля stock_account:

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

Примеры

Запускает тесты из модуля sale:

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

Запускает тесты из модуля sale кроме тех, которые имеют тег slow:

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

Запускает тесты из модуля stock имеющие тег slow:

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

Тестирование 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 веб-браузера.

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

Инфраструктура тестирования

Вот краткий обзор самых важных частей инфраструктуры тестирования:

  • есть бандл с именем web.js_tests_assets. Он содержит основной код (assets common + assets backend), долпнительные библиотеки, средство выполнения тестов QUnit и вспомогательный код
  • другой бандл, web.qunit_suite, содержит все тесты (и код js_tests_assets). Почти все файлы тестов должны быть добавлены в этот бандл
  • есть controller в модуле web, обрабатывающий url-маршрут /web/tests. Этот контроллер рендерит шаблон web.qunit_suite.
  • для выполнения тестов можно просто указать браузеру url-маршрут /web/tests. В этом случае браузер загрузит все ресурсы, и QUnit начнет свою работу.
  • часть кода находится в qunit_config.js , он регистрирует в консоли информацию, когда тест проходит или не проходит.
  • мы хотим, чтобы runbot также запускал эти тесты, поэтому есть тест (в test_js.py), который просто порождает браузер и передает ему URL web/tests. Обратите внимание, что метод phantom_js порождает не phantom_js, а Chrome в headless режиме.

Модульность и тестирование

Благодаря внутренней архитектуре Odoo , любой модуль может изменять поведение других частей системы. Например, дополнение voip может изменить виджет FieldPhone для использования дополнительных функций. С точки зрения системы тестирования это не очень хорошо, так как означает, что тест в модуле web будет провален всякий раз, когда модуль voip установлен (обратите внимание, что runbot запускает тесты со всеми установленными модулями).

В то же время наша система тестирования хороша, потому что она может обнаруживать ситуации когда другой модуль нарушает какие-либо основные функции. Общего решения этой проблемы нет. На данный момент мы создаем решение для каждого конкретного случая.

Как правило, не стоит изменять чье-либо поведение. В нашем примере с voip лучше добавить новый виджет FieldVOIPPhone и изменить несколько представлений, которым он нужен. Таким образом, виджет FieldPhone не затрагивается, и оба могут быть протестированы.

Добавление нового тест кейса

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

  1. создайте новый файл my_addon/static/tests/utils_tests.js. Этот файл содержит основной код для добавления js модуля 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" name="my addon tests" inherit_id="web.qunit_suite">
            <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. Эти функции используют mock server. Это класс javascript, который имитирует ответы на самые распространенные методы модели: read, search_read, nameget, ….
  • Помощники DOM: полезны для имитации событий/действий преследющих определенную цель. Например, testUtils.dom.click выполняет клик по целевому элементу. Обратите внимание, что это безопаснее, чем делать это вручную, потому что он также проверяет, что цель существует и видна.
  • создание помощников: они, вероятно, являются наиболее важными функциями, экспортируемыми test_utils.js. Эти помощники полезны для создания виджетов, с имитацией окружения и множеством мелких деталей, чтобы максимально симулировать реальные условия. Самое важное, безусловно, createView.
  • qunit assertions: QUnit может быть расширен с помощью специальных утверждений. Для Odoo мы часто тестируем некоторые свойства DOM. Вот почему мы сделали свои утверждения. Например, утверждение containsOnce принимает виджет/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).

Трюки

  • 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: большинство служебных функций создания имеют режим отладки (активируется параметром debug: true). В этом случае целевой виджет будет помещен в DOM вместо специального скрытого элемента Qunit, и будет записано больше информации. Например, все смоделированные сетевые коммуникации будут доступны в консоли.
  • при работе над падающим тестом обычно добавляют флаг отладки, а затем комментируют конец теста (в частности, вызов destroy). Благодаря этому можно непосредственно видеть состояние виджета и, более того, управлять им.

Интеграционное тестирование

Тестирование кода Python и кода JS по отдельности очень полезно, но это не доказывает, что веб-клиент и сервер работают вместе. Чтобы сделать это, мы можем написать другой вид теста: туры. Тур - это мини-сценарий описывающи бизнес-процесс. Он объясняет последовательность шагов, которые должны быть выполнены. Затем организатор теста создаст браузер phantom_js, укажет ему правильный URL-адрес и смоделирует клики и ввод данных в соответствии со сценарием.

Скриншоты и скринкасты во время выполнения тестов browser_js

При запуске тестов, использующих HttpCase.browser_js из командной строки, браузер Chrome используется в режиме headless. По умолчанию, если тест не пройден, сохраняется снимок экрана в момент сбоя и записывается в PNG файл

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

Начиная с Odoo 13.0 были добавлены два новых аргумента командной строки: --screenshots и --screencasts