Существует множество способов протестировать приложение. В 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
Предупреждение
Модули тестирования, которые импортируются не из tests/__init__.py
запускаться не будут
При тестировании будет запускаться любой тест, как описано в официальной unittest documentation, но Odoo предоставляет ряд утилит и помощников, связанных с тестированием контента Odoo (главным образом модулей):
class odoo.tests.common.TransactionCase(methodName='runTest')[исходный код]
TestCase, в котором каждый тестовый метод запускается в своей транзакции и с собственным курсором. Откат транзакции и закрытие курсора после каждого теста.
browse_ref(xid)[исходный код]
Возвращает объект записи для предоставленного external identifier
module.identifier
ref(xid)[исходный код]
Возвращает ID базы данных для предоставленного external identifier, ярлык для get_object_reference
module.identifier
class odoo.tests.common.SingleTransactionCase(methodName='runTest')[исходный код]
TestCase, в которой все тестовые методы запускаются в одной транзакции, транзакция запускается с помощью первого метода тестирования и откатывается в конце последнего.
browse_ref(xid)[исходный код]
Возвращает объект записи для предоставленного external identifier
module.identifier
ref(xid)[исходный код]
Возвращает ID базы данных для предоставленного external identifier, ярлык для get_object_reference
module.identifier
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
module.identifier
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
module.identifier
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...
Примечание
Методы тестов должны начинаться с test_
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 помечает поле как измененное, даже если установлено его текущее значение
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
.
Форма создается из представления списка, если он доступен для редактирования, или в противном случае из формы поля.
new()[исходный код]
Возвращает класс Form
для новой One2many
,правильно инициализированной,записи.
Форма создается из представления списка, если он доступен для редактирования, или в противном случае из формы поля.
remove(index)[исходный код]
Удаляет запись в index
из родительской формы.
Выполнение тестов
Тесты автоматически запускаются при установке или обновлении модулей, если опция --test-enable
была включена при запуске сервера Odoo.
Выбор теста
В Odoo тесты Python могут быть помечены тегом для облегчения выбора тестов при их запуске.
Подклассы odoo.tests.common.BaseCase
(обычно через TransactionCase
, SavepointCase
или HttpCase
) автоматически помечаются как standard
, at_install
и именем их исходного модуля по умолчанию.
Введение
--test-tags
может использоваться для выбора/фильтрации тестов для запуска с помощью командной строки.
Эта опция по умолчанию имеет значение +standard
что означает, что тесты с тегом standard
(явно или неявно) будут запускаться по умолчанию при запуске Odoo с параметром --test-enable
.
При написании тестов декоратор tagged()
можно использовать в классах тестов для добавления или удаления тегов.
Аргументы декоратора - это имена тегов в виде строк.
Опасно
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):
...
Примеры
Важно
Тесты будут выполняться только в установленных или обновленных модулях. Поэтому модули нужно выбирать с помощью парметеров -u
или -i
. Для простоты эти параметры не указаны в приведенных ниже примерах.
Запускает тесты из модуля 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'
Примечание
``-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 веб-браузера.
Веб-интерфейс имеет много полезных функций: он может запускать только некоторые подмодули или фильтровать тесты, соответствующие строке. Он может показать все утверждения, неудачные или пройденные, перезапустить определенные тесты, …
Предупреждение
Во время работы тестов убедитесь, что:
- что окно вашего браузера находится в фокусе
- он не увеличен/уменьшен. Он должен иметь ровно 100% уровень масштабирования.
Если это не так, некоторые тесты не пройдут без надлежащего описания.
Инфраструктура тестирования
Вот краткий обзор самых важных частей инфраструктуры тестирования:
- есть бандл с именем 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). Процесс добавления нового тестового кейса следующий:
создайте новый файл 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'); }); });
В файле 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>
- Перезапустите сервер и обновите my_addon, или сделайте это из интерфейса (чтобы убедиться, что новый файл теста загружен)
Добавьте тестовый кейс после определения набора субтестов 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); });
- Перейдите по ссылке /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).
Трюки
- для выполнения только одного теста: вы можете (временно!) изменить определение QUnit.test(…) на QUnit.only(…). Это полезно, чтобы убедиться, что QUnit запускает только этот конкретный тест.
- флаг 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