.. _gsauto_ug_parallel_execution: Параллельное выполнение тестов =============================== Тесты, написанные с использованием `gs-automation`, часто запускаются не по одному, а параллельно — несколькими одновременно работающими воркерами Gradle в рамках одного тестового прогона. Каждый воркер поднимает свой собственный экземпляр веб-браузера и свою сессию клиента, но все они работают с одним и тем же сервером приложений Global3 и одной и той же базой данных. Большинство тестов от этого никак не страдают — они открывают собственные формы и работают только с теми данными, которые сами же создают или читают. Но есть класс сценариев, в которых параллельные, никак не связанные друг с другом тесты, могут случайно "столкнуться" на одной и той же записи. Эта глава рассказывает, откуда берётся такое столкновение и как писать тесты, устойчивые к нему. Монопольная блокировка редактирования -------------------------------------- Прикладной код Global3 может защищать записи справочников от одновременного редактирования разными пользователями: как только запись открывается на редактирование, сервер накладывает на неё монопольную блокировку. Если кто-то другой попытается отредактировать ту же самую запись, пока блокировка не снята, он получит от сервера ошибку, например, с текстом вида: .. code-block:: none Данный объект '...' - '...' сейчас редактируется пользователем с учетной записью '...', поэтому не может быть изменен вами. Для настоящих пользователей это ожидаемое и полезное поведение. Но для параллельно запущенных тестов оно превращается в источник случайных, не связанных с существом теста падений: если два независимых теста, выполняющихся в разных воркерах, оба обращаются к одной и той же — например, единственной "тестовой" — записи справочника, один из них гарантированно получит такую ошибку, хотя оба теста сами по себе написаны правильно. .. attention:: Блокировка проверяется только при редактировании **уже существующей** записи. Создание новой записи блокировку не проверяет и никогда не приводит к описанной выше ошибке — на этом факте строятся оба решения, рассмотренные ниже. Принцип: не завязывайтесь на общую запись ------------------------------------------- Если тестовый сценарий обращается к конкретной, заранее известной записи — по её идентификатору или по индексу строки в таблице — он неявно предполагает, что эта запись принадлежит только ему. При параллельном выполнении это предположение может оказаться неверным: точно такая же запись может понадобиться другому тесту, выполняющемуся в этот момент в соседнем воркере. Не всегда можно заранее сказать, защищена ли конкретная выборка монопольной блокировкой. Поэтому для любой выборки, работающей с настоящими, сохраняемыми в базе записями, безопаснее сразу выбирать один из двух приёмов ниже, а не полагаться на то, что "тестовая" запись используется только вашим тестом. Карточное представление: открывайте новую запись ---------------------------------------------------- Если тесту не важно, с какой именно записью он работает — например, он проверяет только структуру карточки, состав полей или сам факт её открытия и закрытия — самый простой и надёжный приём состоит в том, чтобы вместо открытия карточки на редактирование существующей записи, открыть её на **создание новой**. .. code-block:: java :caption: Открытие карточки на создание новой записи вместо редактирования существующей application.mainForm().mainMenu().itemByCaption("Формы", "Справочник с MDI-карточкой").click(); Form listForm = application.waitMdiForm("gtk-ru.bitec.app.gs3.qa.form.open_card_type.Gs3_QAReferenceMdiCard", "List"); // "Создать" вместо "Редактировать" -- открывается новая, никому больше не видимая запись listForm.mainSelection().layout().frame().toolbar().buttonByCaption("Создать").click(); Form cardForm = application.waitMdiForm("gtk-ru.bitec.app.gs3.qa.form.open_card_type.Gs3_QAReferenceMdiCard", "Card"); // ... проверки, не зависящие от конкретной записи ... Так как запись ещё не существует в базе, ни о какой блокировке речи не идёт — два параллельных теста, каждый из которых открывает свою собственную новую запись, никогда не столкнутся. .. note:: После отказа от сохранения такой карточки (``StandardOperations.CLOSE_FORM_CANCEL``) сервер может, в зависимости от выборки, показать диалог подтверждения отказа от несохранённых изменений. Его нужно обработать, как описано в :ref:`gsauto_ug_dialogs`: .. code-block:: java :caption: Закрытие карточки новой записи с возможным диалогом подтверждения cardForm.mainSelection().layout().frame().toolbar().button(StandardOperations.CLOSE_FORM_CANCEL).click(); try { application.waitMsgDialog().no(); } catch (ElementNotFoundException ignored) { // диалог не появился -- отказывать не от чего, несохранённая запись и так пропадёт } cardForm.waitClosing(); Табличное представление: заведите собственную изолированную запись ---------------------------------------------------------------------- Приём с открытием на создание годится не всегда: например, тест может проверять поведение редакторов ячеек таблицы, а для этого нужна именно сохранённая запись. В таком случае решение — не переиспользовать чужую "тестовую" запись, а завести перед тестом **свою собственную**, работать только с ней и удалить её после теста. Такую подготовку и очистку данных удобно возложить на прикладной код в виде операции и выполнять её аннотациями ``@Oper``/``@Jexl``, как описано в главе :ref:`gsauto_ug_annotations`. Операция создания должна возвращать идентификатор созданной записи, чтобы тест мог её затем найти и, по завершении, удалить. .. code-block:: java :caption: Создание собственной изолированной записи перед тестами public class GridEditorsTest extends GsAutomationJUnitEnvironment { private static final String LIST_FORM = "gtk-ru.bitec.app.gs3.qa.controls.Gs3_QaEditorsTest#List"; private Form listForm; private Long isolatedRecordId; @Override protected void onOperationExecuted(String methodName, String formName, String selectionName, String operationName, Object result) { if ("qa_createIsolatedRecord".equals(operationName)) { isolatedRecordId = Long.valueOf((String) result); } } @BeforeAll @Oper(name = "qa_createIsolatedRecord", form = LIST_FORM) public void beforeAll() { application.mainForm().mainMenu().itemByCaption("Редакторы", "В списке").click(); listForm = application.waitMdiForm(LIST_FORM); } } Найти свою запись среди остальных строк таблицы после её создания нужно по содержимому, а не по индексу — индекс строки зависит от текущей сортировки и от того, сколько записей уже успели создать другие параллельные тесты. .. code-block:: java :caption: Поиск своей изолированной записи по содержимому строки Grid grid = listForm.mainSelection().layout().frame().view().cast(); String expectedCaption = "isolated-" + isolatedRecordId; Row foundRow = null; for (Row row : grid.getRows()) { if (expectedCaption.equals(row.getCellByName("SCAPTION").value())) { foundRow = row; break; } } Assertions.assertNotNull(foundRow, "Изолированная запись не найдена в таблице"); По окончании теста запись нужно удалить тем же образом, каким она была создана — кликом по найденной строке и кнопке ``StandardOperations.DELETE`` на панели инструментов. .. attention:: Кнопка панели инструментов действует на строку, **реально выделенную кликом**, а не на ту, что была сфокусирована на сервере, например, методом :java:meth:`Selection#locate(Map) `. Поэтому перед кликом по кнопке ``DELETE`` найденную строку нужно сначала кликнуть: .. code-block:: java :caption: Удаление найденной изолированной записи foundRow.click(); frame.toolbar().button(StandardOperations.DELETE).click(); frame.toolbar().button(StandardOperations.SAVEFORM).click(); .. note:: Метод :java:meth:`Grid#getSelectedRowVisibleIndex() `, удобный для определения текущей выделенной клиентом строки, не всегда надёжно отражает результат серверного ``locate()`` — особенно после переоткрытия формы или при нескольких одновременно работающих сессиях. Для поиска именно своей записи среди строк таблицы надёжнее сканирование содержимого строк, как показано выше, а не сочетание ``locate()`` + ``getSelectedRowVisibleIndex()``. .. seealso:: - Руководство пользователя: :ref:`gsauto_ug_annotations` - Руководство пользователя: :ref:`gsauto_ug_selection` - Руководство пользователя: :ref:`gsauto_ug_grid` - Руководство пользователя: :ref:`gsauto_ug_dialogs` - Руководство пользователя: :ref:`gsauto_ug_junit`