Автоматические тесты NUnit¶
Системные требования¶
Разработка и выполнение тестов в IDE¶
К программному обеспечению компьютера разработчика предъявляются требования, аналогичные требованиям, предъявляемым при разработке расширений.
Выполнение тестов из консоли¶
Windows¶
-
Windows 7 SP1 / 8.1 / 10 / 11
-
.NET SDK 5.0.xxx последней доступной версии.
Linux¶
-
Поддерживаемая версия ОС. Список поддерживаемых версий см. в Руководстве по установке СЭД TESSA на Linux.
-
.NET SDK 5.0.xxx последней доступной версии для используемого дистрибутива Linux.
-
Для удалённой отладки потребуется установить SSH-сервер. Подробнее см. в Удаленная отладка .NET в Linux с помощью SSH.
Сборки с тестами¶
Классы с тестами располагаются в специальных сборках с клиентскими или серверными тестами.
Tessa.Test.Server.dll
сборка, содержащая тесты, выполняющие тестирование функциональности, доступной на сервере.
Tessa.Test.Client.dll
сборка, содержащая тесты, выполняющие тестирование функциональности, доступной на клиентах TessaClient и TessaAdmin.
Tessa.Test.Windows.dll
сборка, содержащая тесты, выполняющие тестирование функциональности, доступной на клиентах TessaClient и TessaAdmin с поддержкой пользовательского интерфейса.
Note
-
Клиентские тесты могут выполняться при подключении к существующему серверу приложений или на специально созданном, в основе которого находится TestServer.
-
Клиентские тесты не предоставляют механизмов для тестирования клиентских расширений, взаимодействующих с пользовательским интерфейсом.
-
В отличии от клиентских расширений, в клиентских тестах доступно прямое обращение к базе данных.
Tessa.Test.Shared.dll
сборка, содержащая классы, предоставляющие функциональность, используемую как в серверных, так и в клиентских тестах.
Tessa.Test.Default.(Server|Client|Windows|Shared).dll
сборки, содержащие классы, предоставляющие типовую функциональность для проведения тестирования.
Советы по использованию различных видов тестов. Список не является исчерпывающим.
Используйте серверные тесты, если:
-
Требуется провести тестирование объекта, зарегистрированного в контейнере Unity, доступного на сервере.
-
Требуется провести тестирование работы обработчика этапа маршрута или действия бизнес-процесса Workflow Engine.
-
Требуется провести тестирование работы маршрута документов или бизнес-процесса Workflow Engine.
-
Требуется провести тестирование работы настройки типа документа или типа карточки.
-
Требуется провести тестирование работы серверного расширения, управляющего правами на доступ к определённым данным карточки.
Используйте клиентские тесты без поддержки пользовательского интерфейса, если:
-
Требуется провести тестирование объекта, зарегистрированного в контейнере Unity, доступного на клиенте, но не зависящего от
Tessa.UI.dll
. -
Требуется провести тестирование обращений к веб-сервису с клиентской стороны.
-
Требуется выполнять клиентские тесты на ОС Linux.
Используйте клиентские тесты с поддержкой пользовательского интерфейса, если:
-
Требуется провести тестирование объекта, зарегистрированного в контейнере Unity, доступного на клиенте и имеющего зависимость от
Tessa.UI.dll
. -
Требуется провести тестирование расширений моделей элементов рабочего места
Tessa.UI.Views.Extensions.IWorkplaceExtension<out TModel>
. -
Требуется провести тестирование обращений к веб-сервису с клиентской стороны.
Настройка файлов конфигурации¶
Проекты: Source\Tests\Tessa.Test.Client
, Source\Tests\Tessa.Test.Server
и Source\Tests\Tessa.Test.Windows
содержат файлы конфигурации: app.json
, extensions.xml
и NLog.config
.
Файл app.json¶
Содержит настройки, используемые при выполнении тестов.
Note
При написании файлов app.json
необходимо учитывать следующие особенности. Должен выполняться эскейпинг символа обратного слеша, т.е. пишем \\
вместо одного \
, это часть стандарта JSON. В начале любого из значений можно написать символ @
, это вставит путь к папке с текущим файлом app.json
и обратным слешем. Например, файл лежит в папке Source\Tests\Tessa.Test.Server\bin\Debug\net5.0
и есть настройка с путём к файловому хранилищу @Files
, то после обработки файла значение будет равно Source\Tests\Tessa.Test.Server\bin\Debug\net5.0\Files
. Это применимо к любым настройкам в app.json
, но не является частью стандарта JSON и не будет работать для других .json
-файлов. Для того, чтобы в начале значения действительно вставить символ @
вместо пути, то его надо написать дважды @@
.
Общие параметры¶
В файле необходимо настроить следующие параметры (выделено жёлтым):
-
Параметр:
Строка подключения.
“ConnectionStrings”: { “default”: “Server=.\SQLEXPRESS;Database=tessa_test;Integrated Security=false;User ID=sa;Password=Master1234;Pooling=true;Max Pool Size=1024”, “temp_ms”: “Server=.\SQLEXPRESS;Database=tessa_test;Integrated Security=false;User ID=sa;Password=Master1234;Pooling=true;Max Pool Size=1024”, “temp_pg”: [ “Host=localhost;Database=tessa_test;Integrated Security=false;User ID=postgres;Password=Master1234;Pooling=false;Timeout=0”, “Npgsql” ], “gc”: “Filename=C:\Tessa-Test\gclocal.db;Connection=shared” }
Параметр содержит:
-
Строки подключения к тестовой базе данных TESSA.
-
default
– строка подключения по умолчанию. Используется, если подключение явно не задано. -
temp_ms
– строка подключения к временной базе данных SQL Server. -
temp_pg
– строка подключения к временной базе данных PostgreSQL.
Информацию о формате строки подключения см. в Руководстве по установке СЭД TESSA.
Строка подключения зависит от используемого способа выполнения тестов: на временной базе или на постоянной.
Если тесты выполняются на постоянной базе, то необходимо указать в строке подключения по умолчанию (
default
) параметры подключения к существующей базе данных, иначе можно указать любое допустимое имя, например,tessa-test
, в этом случае база данных, с именем, начинающимся на указанную строку, будет автоматически создана. -
-
Служебные строки подключения.
-
gc
– строка подключения к постоянной служебной базе данных используемой в тестах для хранения информации о внешних временных ресурсах.Подробнее см. в пункте Сборка мусора. Формат строки подключения см. в руководстве по LiteDB.
Warning
Для полноценной работы база данных с информацией о внешних объектах должна располагаться в месте защищённом от случайного удаления. Иначе информация об отслеживаемых объектах будет потеряна.
Не изменяйте тип подключения к базе данных. Это может привести к невозможности параллельного выполнения тестов.
-
-
-
“FileStoragePath”: “C:\Tessa-Test\Files“
Путь к файловому хранилищу.
Зависит от используемой базы данных:
-
Постоянная база данных.
Путь к существующему файловому хранилищу, заданному в параметрах сервера.
-
Временная база данных.
Любой допустимый путь.
-
-
Параметр:
“FileUseSimpleNamingScheme”: false
Использовать режим обратной совместимости при именовании файлов. Данный параметр аналогичен настройке сервера для хранилища файлов “Обратная совместимость”.
-
Параметр:
“FixtureDate”: null
Дата и время, используемые при создании имён временных ресурсов, используемых в тестах. Если задано значение
null
, то используется текущая дата и время.Для получения значения через API используйте
TestSettings.FixtureDate
. Для получения значения, используемого в тестах, используйтеITestNameResolver.GetFixtureDateTimeAsync
. -
Параметр:
“FixtureSeed”: 0
Начальное значение, используемое для вычисления последовательности псевдослучайных чисел, применяемых при создании имён временных ресурсов, используемых в тестах. Если задано не положительное значение или параметр отсутствует, то используется начальное значение по умолчанию для
System.Random
.Для получения значения через API используйте
TestSettings.FixtureSeed
. -
Параметр:
“GCKeepAliveInterval”: “06:00:00”
Время жизни внешних объектов, оставшихся после выполнения тестов. Значение должно быть не отрицательным.
Величину данного параметра рекомендуется устанавливать не менее максимально возможной продолжительности общего времени выполнения тестов из групп тестов (test fixture), в которых используется общая база данных или область выполнения.
Значение можно задать равным нулю (
"00:00:00"
), если не выполняется параллельных запусков тестов, использующих внешние временные ресурсы.Для получения значения через API используйте
TestSettings.GCKeepAliveInterval
.Подробнее про механизм сборки мусора см. в пункте Сборка мусора.
-
“UseTestScope”: true
Разрешено использовать области выполнения.
Для получения значения через API используйте
TestSettings.UseTestScope
.
Параметры, используемые только в клиентских тестах¶
-
Параметр:
“BaseAddress”: “https://localhost/tessa“
Базовый адрес сервера, к которому будет выполняться подключение.
-
Параметр:
“OpenTimeout”: “00:01:01“
Таймаут на открытие соединения с сервером.
-
Параметр:
“CloseTimeout”: “00:01:02“
Таймаут на закрытие соединения с сервером.
-
Параметр:
“SendTimeout”: “00:40:00“
Таймаут на отправку данных на сервер.
-
“UserName”: “admin“
Логин (имя пользователя) для учётной записи Windows вместе с указанием его домена в том же виде, в каком задано в справочнике сотрудников, или логин пользователя, авторизация которого выполняется средствами TESSA.
Если не задан, то используется аутентификация пользователя по учётной записи Windows.
-
Параметр:
“Password”: “admin“
Пароль для учётной записи Windows или для записи пользователя TESSA.
Если не задан, то используется аутентификация пользователя по учётной записи Windows.
Информация по параметрам, отсутствующим в данном разделе, содержится в Руководстве по установке → Настройка конфигурационного файла.
Файл extensions.xml¶
Файл содержит список сборок с расширениями (extensions.xml
могут ссылаться друг на друга, как пример extensions.xml
в папке TESSA типовой сборки ссылается на файл extensions.xml
в папке extensions
).
Пример файла extensions.xml для серверных тестов (Tessa.Test.Server)
<?xml version="1.0" encoding="utf-8" ?>
<extensions xmlns="http://syntellect.ru/tessa/include">
<include file="Tessa.Extensions.Default.Shared.dll" />
<include file="Tessa.Extensions.Default.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.Shared.dll" />
<include file="Tessa.Extensions.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.PostgreSql.Server.dll" serverOnly="true" />
</extensions>
Пример файла extensions.xml для клиентских тестов, не использующих Windows-зависимости (Tessa.Test.Client)
<?xml version="1.0" encoding="utf-8" ?>
<extensions xmlns="http://syntellect.ru/tessa/include">
<include file="Tessa.Extensions.Default.Shared.dll" />
<include file="Tessa.Extensions.Default.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.Shared.dll" />
<include file="Tessa.Extensions.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.Default.Server.Web.dll" serverOnly="true" />
<include file="Tessa.Extensions.Server.Web.dll" serverOnly="true" />
<include file="Tessa.Extensions.PostgreSql.Server.dll" serverOnly="true" />
</extensions>
Пример файла extensions.xml для клиентских тестов с поддержкой пользовательского интерфейса (Tessa.Test.Windows)
<?xml version="1.0" encoding="utf-8" ?>
<extensions xmlns="http://syntellect.ru/tessa/include">
<include file="Tessa.Extensions.Default.Shared.dll" />
<include file="Tessa.Extensions.Shared.dll" />
<include file="Tessa.Extensions.Default.Client.dll" clientOnly="true" />
<include file="Tessa.Extensions.Client.dll" clientOnly="true" />
<include file="Tessa.Extensions.Default.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.Default.Server.Web.dll" serverOnly="true" />
<include file="Tessa.Extensions.Server.Web.dll" serverOnly="true" />
<include file="Tessa.Extensions.PostgreSql.Server.dll" serverOnly="true" />
</extensions>
Файл NLog.config¶
По умолчанию включено логирование на уровне Info
.
Подробную информацию по настройке файла конфигурации NLog.config
см. в NLog/Configuration file.
Файл appsettings.json¶
Содержит параметры тестового сервера, используемого в клиентских тестах, выполняемых на временной базе данных.
По умолчанию определяет уровни ведения журнала. Подробную информацию по настройке ведения журнала см. в Ведение журнала в .NET Core и ASP.NET Core.
Выполнение тестов¶
Подготовительные действия¶
-
Серверные тесты
-
При выполнении тестов на постоянной базе данных необходимо в строке подключения файла конфигурации серверных тестов
Source\Tests\Tessa.Test.Server\app.json
указать подключение к базе данных, которая будет использоваться при выполнении тестов. -
Обновить тестовую конфигурацию, выполнив пакетный файл
Configuration.Test\Update.bat
. -
После обновления тестовой конфигурации необходимо пересобрать проекты, содержащие тесты.
-
-
Клиентские тесты
Клиентские тесты могут выполняться на постоянной или временной базе данных.
-
Выполнение тестов на постоянной базе данных.
-
Выполнить установку нового экземпляра системы TESSA с указанием тестовой базы данных.
-
Проверить правильность заданных параметров в файлах конфигурации.
Note
-
Для проведения тестирования не требуется выполнять настройку
Tessa Applications
. -
Сервис Chronos не требуется устанавливать в качестве системной службы. Его можно запустить один раз после завершения установки.
-
В строке подключения файла конфигурации клиентских тестов
Source\Tests\Tessa.Test.Client\app.json
необходимо указать подключение к базе данных, на которую была выполнена установка нового экземпляра системы.
-
-
Выполнение тестов на временной базе данных.
-
Обновить тестовую конфигурацию, выполнив пакетный файл
Configuration.Test\Update.bat
. -
После обновления тестовой конфигурации необходимо пересобрать проекты, содержащие тесты.
-
-
Выполнение и отладка тестов в IDE¶
В данном разделе кратко рассматривается выполнение и отладка тестов в IDE на примере Microsoft Visual Studio 2019. При использовании другой IDE – обратитесь к её документации.
Для просмотра и выполнения тестов используется Обозреватель тестов. Открыть Обозреватель тестов можно через меню Visual Studio: Вид → Обозреватель тестов или Тест → Обозреватель тестов.
В верхней части расположены кнопки, позволяющие: запустить, выполнить фильтрацию, настроить отображение и выполнение тестов.
В контекстном меню, доступном на элементах дерева представляющего список тестов, доступны команды, позволяющие запустить тесты для выполнения и отладки, а также выполнить переход к исходному коду теста.
Для отладки теста необходимо:
-
Установить точки останова в исходном коде отлаживаемых тестов.
-
Запустить отлаживаемые тесты, выбрав в контекстном меню элемента Обозревателя тестов пункт Отладка.
Также, можно запускать тесты в режиме отладки из меню кнопки Запустить, расположенной на панели инструментов Обозревателя тестов или Меню Visual Studio → Тест.
Отладка тестов в IDE на Linux¶
Предварительные требования¶
-
На компьютере с Visual Studio необходимо установить рабочую нагрузку ASP.NET и разработка веб-приложений или Кроссплатформенная разработка .NET.
-
Установить SSH сервер.
Для установки выполните следующую команду в консоли:
sudo apt-get install openssh-server unzip curl
Создание и развёртывание приложения¶
Подготовка приложения для отладки.
-
Выберите конфигурацию проекта Отладка.
-
Убедитесь, что проект настроен на создание переносимых PDB-файлов (параметр по умолчанию), что PDB-файлы находятся в том же расположении, что и библиотека DLL. Чтобы выполнить эту настройку в Visual Studio, щёлкните проект правой кнопкой мыши, затем выберите Свойства → Сборка → Дополнительно → Сведения об отладке.
Note
Проекты
Tessa.Test.(Client | Server | Share)
настроены на создание переносимых PDB-файлов.
Для развёртывания приложения перед отладкой можно использовать несколько методов.
-
Скопируйте источники на целевой компьютер и выполните сборку с помощью
dotnet build
илиdotnet test
на компьютере Linux. -
Выполните сборку приложения в Windows, а затем перенесите артефакты сборки на компьютер Linux (артефакты сборки состоят из самого приложения, любых библиотек среды выполнения, от которых он может зависеть, и файла
*.deps.json
).
Note
Для упрощения изложения в примерах будет использоваться способ запуска тестов из сборки, собранной на Windows.
Подключение отладчика¶
-
Включение режима ожидания подключения отладчика к хост-процессу, выполняющему тесты.
В консоли необходимо выполнить команду, которая установит переменную окружения, включающую режим ожидания подключения отладчика к процессу
dotnet
:export VSTEST_HOST_DEBUG=1
-
Запуск тестов.
В этой же консоли необходимо выполнить команду, запускающую тесты на выполнение, например, следующей командой (подробное описание команд
dotnet test
см. в п. Выполнение тестов из консоли):dotnet test Tessa.Test.Server.dll
вместо выполнения тестов
dotnet test
будет ожидать подключения отладчика. В сообщении будет указан идентификатор и имя процесса, к которому необходимо подключиться.На приведённом рисунке это:
-
имя процесса: dotnet;
-
идентификатор процесса: 102967.
-
-
Присоединение к процессу.
В Visual Studio следует открыть окно Присоединиться к процессу… (Меню Visual Studio → Отладка → Присоединиться к процессу…) и указать параметры подключения:
-
Тип подключения:
SSH
-
Цель подключения: Можно оставить пустым или указать строку подключения имеющую следующий формат: [<Имя пользователя>@]< <IP адрес> | <Имя компьютера> >
-
Нажмите кнопку Обновить или при установленном курсоре в поле Цель подключения нажмите клавишу [Enter]. Если подключение выполняется первый раз или не задана Цель подключения, будет открыто окно Подключение к удалённой системе. Заполните необходимые параметры подключения.
И нажмите кнопку Подключить. В дальнейшем сохранёнными подключениями можно управлять в меню Диспетчер подключений (Меню Visual Studio → Средства → Параметры → Кросс-платформенные → Диспетчер подключений).
-
Выберите в списке Доступные процессы процесс с идентификатором, указанным в выводе команды
dotnet test
. В примере – 102967. Можно воспользоваться поиском. -
Присоединитесь к процессу, нажав кнопку Присоединиться или два раза щелкните по строке с процессом левой кнопкой мыши. После чего откроется окно Присоединить к “dotnet” – выбор типа кода. Выберите тип кода Управляемый (.NET для Unix) и нажмите кнопку ОК.
После чего произойдёт подключение отладчика к процессу и начнётся загрузка символов.
Если в коде теста установлена точка останова, то выполнение будет на ней приостановлено, иначе выполнение будет происходить до тех пор, пока не возникнет исключение, обрабатываемое в соответствии с параметрами, заданными в окне Параметры исключений (Меню Visual Studio → Отладка → Окна → Параметры исключений), или до завершения выполнения теста.
Tip
Информацию о ходе отладки можно просмотреть в окне Вывод (Меню Visual Studio → Отладка → Окна → Вывод или Меню Visual Studio → Вид → Вывод) при выбранных выходных данных Отладка.
Выполнение тестов из консоли¶
Для выполнения тестов из консоли на Windows и Linux используется драйвер тестов .NET dotnet test
, входящий в .NET SDK.
Основные команды:
dotnet test [<PROJECT> | <SOLUTION> | <DIRECTORY> | <DLL>]
[--filter <EXPRESSION>]
[-l|--logger <LOGGER_URI/FRIENDLY_NAME>]
[-c|--configuration <CONFIGURATION>]
[--no-build]
[-o|--output <OUTPUT_DIRECTORY>]
[-r|--results-directory <PATH>]
[-t|--list-tests]
[-v|--verbosity <LEVEL>]
Полный список команд доступен в документации или в встроенной справке, доступной по команде:
dotnet test -h
-
PROJECT | SOLUTION | DIRECTORY | DLL
-
PROJECT
- путь к тестовому проекту. -
SOLUTION
- путь к решению. -
DIRECTORY
- путь к каталогу, содержащему проект или решение. -
DLL
- путь к сборке, содержащей запускаемые тесты.Если ни один из аргументов не указан, то выполняется поиск проекта или решения в текущем каталоге.
-
-
--filter <EXPRESSION>
Фильтр, используемый для фильтрации выполняемых тестов. Подробную информацию см. в Сведения о параметре “Фильтр” и Выполнение выборочных модульных тестов.
-
-l|--logger <LOGGER_URI/FRIENDLY_NAME>
Средство ведения журнала результатов тестирования. Список средств см. в Reporting test results.
По умолчанию используется
Console Logger
. -
-c|--configuration <CONFIGURATION>
Конфигурация сборки. Значение по умолчанию -
Debug
. Конфигурация проекта может переопределить значение параметра по умолчанию. -
--no-build
Не выполнять сборку тестового проекта перед запуском.
-
-o|--output <OUTPUT_DIRECTORY>
Каталог, в котором выполняется поиск двоичных файлов для выполнения. Если значение не указано, то используется путь по умолчанию -
./bin/<configuration>/<framework>/
. -
-r|--results-directory <PATH>
Каталог, в котором сохраняются результатов тестов. Если указанный каталог не существует, то он создаётся. По умолчанию используется
TestResults
в каталоге, содержащем файл, заданный какPROJECT | SOLUTION | DIRECTORY | DLL
. -
-t|--list-tests
Отображение списка всех обнаруженных тестов.
-
-v|--verbosity <LEVEL>
Уровень детализации команды. Допустимые значения:
q[uiet]
,m[inimal]
,n[ormal]
,d[etailed]
иdiag[nostic]
. Значение по умолчанию -minimal
. Для получения дополнительной информации см. LoggerVerbosity.
Примеры запуска тестов из консоли¶
В приведённых примерах считается, что команда dotnet test
запускается из директории, содержащей: тестовый проект, решение или сборку с запускаемыми тестами.
-
Выполнение сборки проекта, содержащего тесты и запуск теста, имеющего полное имя
Tessa.Test.Server.TestClass.TestMethodName
.Результаты выполнения будут записаны в файле, расположенном в папке
logs
, расположенной относительно текущей директории.Используемое средство ведения журнала результатов тестирования: Trx Logger. Файл с результатами выполнения можно открыть в Visual Studio, где будет графическое представление результатов тестирования в области Результаты теста или в любом текстовом редакторе – файл имеет XML формат.
dotnet test –results-directory “logs” –logger:”trx” –filter “FullyQualifiedName=Tessa.Test.Server.TestClass.TestMethodName”
-
Выполнение сборки проекта, содержащего тесты и запуск всех тестов, имеющих имя
TestMethodName
.Результаты выполнения будут записаны в файл
result.html
, расположенный по относительному путиlogs
.Используемое средство ведения журнала результатов тестирования: Html Logger.
dotnet test –results-directory “logs” –logger:”html;LogFileName=result.html” –filter Name=TestMethodName
-
Выполнение всех тестов, расположенных в сборке
Tessa.Test.Server.dll
.Результаты выполнения будут записаны в файл
result.html
, расположенный по относительному путиlogs
.Используемое средство ведения журнала результатов тестирования: Html Logger.
dotnet test Tessa.Test.Server.dll –results-directory “logs” –logger:”html;LogFileName=result.html“
-
Выполнение всех тестов, расположенных в сборке
Tessa.Test.Server.dll
, имеющих категориюdb-ms
.Результаты выполнения будут записаны в файл
result.html
, расположенный по относительному путиlogs
.Используемое средство ведения журнала результатов тестирования: Html Logger.
dotnet test Tessa.Test.Server.dll –results-directory “logs” –logger:”html;LogFileName=result.html” –filter Category=db-ms
Если при выполнении теста возникнет исключение или ошибка, то она будет включена в отчёт о выполнении тестов. Формат отчёта зависит от используемого средства ведения журнала результатов тестирования:
-
Console Logger
Команда:
dotnet test Tessa.Test.Server.dll
Пример результата выполнения:
-
Trx Logger
Команда:
dotnet test Tessa.Test.Server.dll –results-directory “logs” –logger:”trx;LogFileName=result.trx”
Пример результата выполнения:
-
Html Logger
Команда:
dotnet test Tessa.Test.Server.dll –results-directory “logs” –logger:”html;LogFileName=result.html”
Пример результата выполнения:
Параллельное выполнение тестов¶
Для управления параллелизмом выполнения тестов предназначены атрибуты NUnit: https://docs.nunit.org/articles/nunit/writing-tests/attributes/parallelizable.html[Parallelizable]
и https://docs.nunit.org/articles/nunit/writing-tests/attributes/levelofparallelism.html[LevelOfParallelism]
.
При выполнении тестов из Обозревателя тестов параллельное выполнение можно включить/выключить через настройку Параллельный запуск для тестов, расположенную в выпадающем меню кнопки Параметры, расположенной в меню обозревателя тестов.
Tip
При выполнении тестов без ограничения на максимальное число одновременно выполняемых тестов с помощью атрибута https://docs.nunit.org/articles/nunit/writing-tests/attributes/levelofparallelism.html[LevelOfParallelism]
могут возникать ошибки, связанные с блокировками.
Уровень параллелизма при выполнении тестов указан на уровне сборок в файле AssemblyInfo.cs
, который расположен в корне проекта с клиентскими или серверными тестами соответственно.
Лицензия¶
Лицензия используется в серверных и клиентских тестах, выполняемых на временной базе данных.
Файл лицензии указывается стандартным способом. Если информация о файле лицензии отсутствует в конфигурационном файле или файл лицензии не найден, то используется временная лицензия, генерируемая автоматически.
Ограничения временной лицензии, используемой в тестах
Свойство |
Значение |
---|---|
Дата выдачи лицензии | Текущее дата и время компьютера |
Дата окончания лицензии | Один месяц начиная с текущей даты и времени компьютера |
Максимальное количество конкурентных сессий | 1 |
Максимальное количество персональных сессий | 0 |
Модули |
|
Описание API тестов¶
Note
Для упрощения работы с тестами мы рекомендуем соблюдать следующие соглашения наименования классов, предоставляющих тесты:
-
Имя класса, содержащего тесты, должно заканчиваться на слово Test;
-
Имя класса, являющегося базовым по отношению к классам, содержащим тесты, должно заканчиваться на TestBase;
-
Имя класса, тесты в котором выполняются на определённой базе данных, должно заканчиваться на SqlServer или PostgreSql, если используется подключение к базе данных SQL Server или PostgreSQL соответственно.
Пример использования API тестов¶
Note
Для загрузки доступен проект, содержащий тесты для примеров процессов, реализованных с использованием подсистемы маршрутов и бизнес-процессов WorkflowEngine.
Проект с тестами доступен для загрузки по ссылке.
Установка примеров:
-
Извлеките из архива папку
Tessa.Test.Examples.Server
в директорию<Папка сборки>\Source\Tests\
. -
Скачайте архивы с примерами использования маршрутов и бизнес-процессов.
-
В папке со сборкой создайте директорию
rnd
. -
Распакуйте содержимое архива с примерами маршрутов в директорию
<Папка сборки>\rnd\RoutingSamples\
. -
Извлеките папку
Configuration
из архива с примерами бизнес-процессов в директорию<Папка сборки>\rnd\WorkflowExamples\
. -
Извлеките папку
Extensions
из архива с примерами бизнес-процессов в директорию<Папка сборки>\Source\
. -
Откройте решение
<Папка сборки>\Source\Tessa.Extensions.sln
и добавьте в него проект<Папка сборки>\Source\Tests\Tessa.Test.Examples.Server\Tessa.Test.Examples.Server.csproj
. Для этого щелкните правкой кнопкой мыши по узлуTests
в обозревателе решений и выберите в контекстном меню Добавить → Существующий проект…, после чего выберите добавляемый проект. -
В решение
<Папка сборки>\Source\Tessa.Extensions.sln
добавьте проекты из п. 7:Tessa.Extensions.WorkflowExamples.Client
,Tessa.Extensions.WorkflowExamples.Server
,Tessa.Extensions.WorkflowExamples.Shared
. Для этого щелкните правкой кнопкой мыши по узлуExtensions
в обозревателе решений и выберите в контекстном меню Добавить → Существующий проект…, после чего выберите добавляемый проект.
На примерах рассмотрим написание простых тестов разных типов. В последующих разделах будет дана подробная информация по API тестов.
Пример простого серверного теста.
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Extensions.Default.Shared.Workflow.KrPermissions;
using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
using Tessa.Platform.Data;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.Kr;
namespace Tessa.Test.Server.Samples
{
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных SQL Server.
/// </summary>
[SetupTempDb(Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)] // Настройка использования подключения к SQL Server.
public sealed class ExamplesTestMsSql :
ExamplesTest
{
}
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных PostgreSQL.
/// </summary>
[SetupTempDb(Dbms.PostgreSql, TestHelper.TempConfigurationStringPg, TestHelper.DbScriptDefaultPg)] // Настройка использования подключения к PostgreSQL.
public sealed class ExamplesTestPostgreSql :
ExamplesTest
{
}
/// <summary>
/// Базовый абстрактный класс с примерами тестов.
/// </summary>
[Parallelizable, Category("Samples")]
public abstract class ExamplesTest :
KrServerTestBase // Для упрощения выполнения тестов и сокращения выполнения конфигурации класс с тестами наследуется от KrServerTestBase.
{
#region Test methods
/// <summary>
/// Проверяет сохранение темы и заполнение полей: <see cref="KrConstants.DocumentCommonInfo.AuthorID"/>, <see cref="KrConstants.DocumentCommonInfo.AuthorName"/>.
/// </summary>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task CreateCard()
{
const string subject = "Test subject";
var clc = await this.CreateCardLifecycleCompanion() // Создание нового объекта, управляющего жизненным циклом карточки, инициализированного идентификатором типа возвращаемым свойством KrServerTestBase.TestCardTypeID.
.Create() // Планирование создания карточки.
.WithDocType(this.TestDocTypeID, this.TestDocTypeName) // Указание типа документа, который должен использоваться при создании карточки.
.SetValue(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.Subject, subject) // Планирование задания значения полю DocumentCommonInfo.Subject.
.Save() // Планирование сохранения карточки.
.GoAsync(); // Выполнение запланированных действий с проверкой результата выполнения.
var actualSubject = clc.TryGetValue<string>(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.Subject); // Получение значения поля DocumentCommonInfo.Subject.
Assert.AreEqual(subject, actualSubject); // Проверка ожидаемого и фактического значений поля DocumentCommonInfo.Subject на равенство.
var actualAuthorID = clc.TryGetValue<Guid?>(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.AuthorID); // Получение значения поля DocumentCommonInfo.AuthorID.
Assert.AreEqual(this.Session.User.ID, actualAuthorID); // Проверка ожидаемого и фактического значений поля DocumentCommonInfo.AuthorID на равенство.
var actualAuthorName = clc.TryGetValue<string>(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.AuthorName); // Получение значения поля DocumentCommonInfo.AuthorName.
Assert.AreEqual(this.Session.User.Name, actualAuthorName); // Проверка ожидаемого и фактического значений поля DocumentCommonInfo.AuthorName на равенство.
}
#endregion
#region Base overrides
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
await base.InitializeCoreAsync(); // Для правильной инициализации тестов необходимо вызвать базовую реализацию текущего метода.
await this.TestConfigurationBuilder
.GetPermissionsConfigurator() // Получение объекта конфигуратора PermissionsConfigurator правил доступа.
.GetPermissionsCard(Guid.NewGuid()) // Задание карточки правила доступа с заданным идентификатором для настройки.
.AddType(this.TestDocTypeID) // Планирование добавления типа карточки, к которому применяется правило доступа.
.AddRole(this.Session.User.ID) // Планирование добавления роли, для которой выдаются указанные права для указанных типов карточек в указанных состояниях.
.ModifyStates(static _ => KrState.DefaultStates) // Планирование изменения списка состояний карточки, в которых будут работать определённые разрешения.
.AddFlags(KrPermissionFlagDescriptors.Full) // Планирование задания всех разрешений.
.Complete() // Возврат к конфигуратору верхнего уровня - TestConfigurationBuilder.
.GoAsync(); // Выполнение запланированных действий.
}
#endregion
}
}
Пример простого клиентского теста.
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Extensions.Default.Shared.Workflow.KrPermissions;
using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
using Tessa.Test.Default.Client.Kr;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.Kr;
namespace Tessa.Test.Client.Samples
{
/// <summary>
/// Класс с примерами тестов.
/// </summary>
[Parallelizable, Category("Samples")]
[SetupDbScope] /* (1) */
public sealed class ExamplesTest :
KrClientTestBase
{
#region Test methods
/// <summary>
/// Проверяет сохранение темы и заполнение полей: <see cref="KrConstants.DocumentCommonInfo.AuthorID"/>, <see cref="KrConstants.DocumentCommonInfo.AuthorName"/>.
/// </summary>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task CreateCard()
{
const string subject = "Test subject";
var clc = await this.CreateCardLifecycleCompanion()
.Create()
.WithDocType(this.TestDocTypeID, this.TestDocTypeName)
.SetValue(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.Subject, subject)
.Save()
.GoAsync();
this.TestCardManagerOnce.DeleteCardAfterTest(clc); /* (2) */
var actualSubject = clc.TryGetValue<string>(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.Subject);
Assert.AreEqual(subject, actualSubject);
var actualAuthorID = clc.TryGetValue<Guid?>(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.AuthorID);
Assert.AreEqual(this.Session.User.ID, actualAuthorID);
var actualAuthorName = clc.TryGetValue<string>(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.AuthorName);
Assert.AreEqual(this.Session.User.Name, actualAuthorName);
}
#endregion
#region Base overrides
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
await base.InitializeCoreAsync();
await this.TestConfigurationBuilder
.GetPermissionsConfigurator()
.GetPermissionsCard(Guid.NewGuid())
.AddType(this.TestDocTypeID)
.AddRole(this.Session.User.ID)
.ModifyStates(static _ => KrState.DefaultStates)
.AddFlags(KrPermissionFlagDescriptors.Full)
.ModifyCard((configurator, c) =>
{
var currentKey = configurator.CurrentKey;
this.TestCardManagerOnce.DeleteCardAfterTest(c, (_, _) =>
{
configurator.Invalidate(currentKey);
return new ValueTask();
});
}) /* (3) */
.Complete()
.GoAsync();
}
#endregion
}
}
-
Используется подключение по умолчанию.
-
Удаление тестовой карточки после завершения всех тестов.
-
Удаление карточки правила доступа после завершения всех тестов.
Обратите внимание на отличия клиентского теста от серверного:
-
Используется подключение по умолчанию.
-
Удаление тестовой карточки после завершения всех тестов.
-
Удаление карточки правила доступа после завершения всех тестов.
Пример простого клиентского теста, выполняемого на временной базе данных с использованием специально созданного сервера.
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Extensions.Default.Shared.Workflow.KrPermissions;
using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
using Tessa.Platform.Data;
using Tessa.Test.Default.Client.Kr;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.Kr;
namespace Tessa.Test.Client.Samples
{
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных SQL Server.
/// </summary>
[SetupTempDb(Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)] /* (1) */
public sealed class ExamplesTestMsSql :
ExamplesTest
{
}
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных PostgreSQL.
/// </summary>
[SetupTempDb(Dbms.PostgreSql, TestHelper.TempConfigurationStringPg, TestHelper.DbScriptDefaultPg)]
public sealed class ExamplesTestPostgreSql :
ExamplesTest
{
}
/// <summary>
/// Базовый абстрактный класс с примерами тестов.
/// </summary>
[Parallelizable, Category("Samples")]
public abstract class ExamplesTest :
KrHybridClientTestBase /* (2) */
{
#region Test methods
/// <summary>
/// Проверяет сохранение темы и заполнение полей: <see cref="KrConstants.DocumentCommonInfo.AuthorID"/>, <see cref="KrConstants.DocumentCommonInfo.AuthorName"/>.
/// </summary>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task CreateCard()
{
const string subject = "Test subject";
var clc = await this.CreateCardLifecycleCompanion()
.Create()
.WithDocType(this.TestDocTypeID, this.TestDocTypeName)
.SetValue(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.Subject, subject)
.Save()
.GoAsync();
var actualSubject = clc.TryGetValue<string>(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.Subject);
Assert.AreEqual(subject, actualSubject);
var actualAuthorID = clc.TryGetValue<Guid?>(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.AuthorID);
Assert.AreEqual(this.Session.User.ID, actualAuthorID);
var actualAuthorName = clc.TryGetValue<string>(KrConstants.DocumentCommonInfo.Name, KrConstants.DocumentCommonInfo.AuthorName);
Assert.AreEqual(this.Session.User.Name, actualAuthorName);
}
#endregion
#region Base overrides
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
await base.InitializeCoreAsync();
await this.TestConfigurationBuilder
.GetPermissionsConfigurator()
.GetPermissionsCard(Guid.NewGuid())
.AddType(this.TestDocTypeID)
.AddRole(this.Session.User.ID)
.ModifyStates(static _ => KrState.DefaultStates)
.AddFlags(KrPermissionFlagDescriptors.Full)
.Complete()
.GoAsync();
}
#endregion
}
}
-
Используется подключение к временной базе данных, как в серверном тесте.
-
Используется базовый класс
KrHybridClientTestBase
.
Обратите внимание на отличия клиентского теста, выполняемого на временной базе данных и специально созданном тестовом сервере, от простого клиентского теста:
-
Используется подключение к временной базе данных, как в серверном тесте.
-
Используется базовый класс
KrHybridClientTestBase
.
Базовые классы для тестов¶
Для упрощения создания тестов существует несколько абстрактных базовых классов.
Базовые классы для тестов
Имя класса |
Описание |
---|---|
Tessa.Test.Default.Shared.TestBase | Абстрактный базовый класс для тестов |
Tessa.Test.Default.Shared.TestBaseWrapper |
Оболочка базового класса для тестов. Предназначена для создания классов с тестами, выполняемыми в различном окружении. Например, на клиенте и сервере. |
Tessa.Test.Default.Shared.ServerTestBase | Базовый абстрактный класс для серверных тестов |
Tessa.Test.Default.Shared.Kr.KrServerTestBase | Базовый абстрактный класс для серверных тестов, выполняющий настройку тестовой базы данных для типового решения и маршрутов |
Tessa.Test.Default.Client.ClientTestBase | Базовый абстрактный класс для клиентских тестов без поддержки пользовательского интерфейса |
Tessa.Test.Default.Client.HybridClientTestBase | Базовый абстрактный класс для клиентских тестов, выполняемых на специально созданном сервере, без поддержки пользовательского интерфейса |
Tessa.Test.Default.Windows.WindowsHybridClientTestBase | Базовый абстрактный класс для клиентских тестов, выполняемых на специально созданном сервере, с поддержкой пользовательского интерфейса |
Tessa.Test.Default.Client.Kr.KrClientTestBase | Базовый абстрактный класс для клиентских тестов с поддержкой типового решения и маршрутов и без поддержки пользовательского интерфейса |
Tessa.Test.Default.Client.Kr.KrHybridClientTestBase | Базовый абстрактный класс для клиентских тестов, выполняемых на специально созданном сервере, с поддержкой типового решения и маршрутов и без поддержки пользовательского интерфейса |
Tessa.Test.Default.Windows.Kr.WindowsKrHybridClientTestBase | Базовый абстрактный класс для клиентских тестов, выполняемых на специально созданном сервере, с поддержкой типового решения и маршрутов и пользовательского интерфейса |
Tessa.Test.Default.Server.WorkflowEngine.WeTestBase | Абстрактный базовый класс для тестов WorkflowEngine |
Tessa.Test.Default.Server.WorkflowEngine.WeScenarioTestBase | Предоставляет базовую функциональность для тестирования процессов WorkflowEngine |
Основным базовым классом для тестов является Tessa.Test.Default.Shared.TestBase
. Он предоставляет следующие члены (показаны не все объекты):
-
Свойства:
-
Session
– возвращает текущую сессию; -
CardRepository
– возвращает репозиторий для управления карточками; -
DefaultCardRepository
– возвращает репозиторий для управления карточками с конфигурацией по умолчанию; -
CardManager
– возвращает объект, управляющий операциями с карточками; -
CardMetadata
– возвращает метаинформацию, необходимую для использования типов карточек совместно с пакетом карточек; -
CardCache
– возвращает кэш карточек; -
CardLifecycleDependencies
– возвращает объект, управляющий удалением карточек после завершения каждого теста; -
TestCardManager
– возвращает объект, управляющий удалением карточек после завершения каждого теста; -
TestCardManagerOnce
– возвращает объект, управляющий удалением карточек после завершения всех тестов, включая дочерние; -
IsInitialized
– возвращает значение, показывающее, что инициализация зависимостей была выполнена; -
TestConfigurationBuilder
– возвращает конфигуратор тестовой базы данных; -
DbFactory
– фабрикаIDbFactory
, с помощью которой можно получить новый объектDbManager
; -
DbScope
– область видимости объектаDbManager
, который заполняется с помощью фабрикиDbFactory
; -
UnityContainer
– контейнер Unity; -
RoleRepository
– возвращает репозиторий для управления ролевой моделью; -
RemoveFileStorageMode
– возвращает или задаёт режим удаления файлового хранилища при запуске/завершении всех тестов;
-
-
Методы:
-
SetUpAsync
– выполняет действия перед выполнением тестов. Выполняет инициализацию зависимостей. Метод выполняется автоматически NUnit; -
SetUpCoreAsync
– выполняется перед выполнением каждого теста; -
InitializeCoreAsync
– выполняет действия перед выполнением тестов. Используйте этот метод, если необходимо выполнить действие один раз для всех тестов из тест-кейса вместо использования метода, отмеченного атрибутомNUnit.Framework.SetUpAttribute
; -
InitializeScopeCoreAsync
– инициализирует область выполнения. Используйте этот метод, если необходимо выполнить действие при инициализации области выполнения. -
NeedInitializeCoreAsync
– выполняется для каждого теста, только если не требовалось выполнять инициализацию зависимостей (свойствоIsInitialized
имеет значениеtrue
); -
TearDownAsync
– выполняет действия при завершении каждого теста. Метод гарантированно будет вызван, даже если возникнет исключение. Метод выполняется автоматически NUnit; -
TearDownCoreAsync
– выполняет действия при завершении каждого теста; -
OneTimeTearDownAsync
– выполняет действия один раз после выполнения всех дочерних тестов. Метод гарантированно будет вызван, даже если возникнет исключение. Метод выполняется автоматически NUnit; -
OneTimeTearDownCoreAsync
– выполняет действия один раз после выполнения всех дочерних тестов; -
OneTimeTearDownScopeCoreAsync
– выполняет действия один раз при освобождении ресурсов области выполнения; -
CreateContainerAsync
– создаёт Unity контейнер; -
InitializeContainerAsync
– инициализирует Unity контейнер. В данном методе необходимо выполнять регистрацию зависимостей, т.к. в нём не выполняется получение зависимостей, кроме необходимых для инициализации контейнера, например,ITestNameResolver
.
-
Базовый класс также реализует интерфейс ITestActionsContainer
. Он позволяет получить доступ к спискам действий посредством метода GetTestActions
.
Порядок выполнения служебных методов и списков действий
Метод/ список действий |
Тип |
---|---|
Инициализация выполнения | |
BeforeInitialize | Список действий |
CreateAndInitializeContainerAsync | Метод |
CreateContainerAsync | Метод |
BeforeInitializeContainer | Список действий |
InitializeContainerAsync | Метод |
AfterInitializeContainer | Список действий |
BeforeInitializeScope | Список действий |
InitializeScopeCoreAsync | Метод |
AfterInitializeScope | Список действий |
InitializeCoreAsync | Метод |
AfterInitialize | Список действий |
BeforeSetUp | Список действий |
SetUpCoreAsync | Метод |
AfterSetUp | Список действий |
NeedInitializeCoreAsync | Метод |
После завершения теста | |
BeforeTearDown | Список действий |
RemoveCardAfterTestAsync | Метод |
TearDownCoreAsync | Метод |
AfterTearDown | Список действий |
После завершения группы тестов | |
BeforeOneTimeTearDown | Список действий |
RemoveCardOnceAfterTestAsync | Метод |
OneTimeTearDownCoreAsync | Метод |
AfterOneTimeTearDown | Список действий |
BeforeOneTimeTearDownScope | Список действий |
OneTimeTearDownScopeCoreAsync | Метод |
AfterOneTimeTearDownScope | Список действий |
Необработанное исключение не прерывает выполнение методов и списков действий, выполняющихся после завершения теста и группы тестов. Информация об исключениях сохраняется в результатах выполнения текущего теста TestHelper.TestExecutionContext.CurrentResult
. Формат и место отображения определяется NUnit: так ошибки инициализации отображаются в результатах выполнения теста, но информация об ошибках при выполнении метода, отмеченного атрибутом OneTimeTearDown
, выводится в окно “Вывод” (Меню Visual Studio → Вид → Вывод) при отображении выходных данных для тестов.
Tip
В библиотеке NUnit есть свойство TestExecutionContext.CurrentContext
, которое возвращает текущий контекст для тестов, но, чтобы избежать получения фейкового контекста, лучше использовать свойство TestHelper.TestExecutionContext
из пространства имен Tessa.Test.Default.Shared
, которое либо вернет текущий контекст, либо выбросит исключение в случае фейкового контекста.
Использование KrServerTestBase
¶
Пример использования KrServerTestBase
.
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Cards;
using Tessa.Extensions.Default.Shared.Workflow.KrPermissions;
using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
using Tessa.Platform.Data;
using Tessa.Platform.Validation;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.Kr;
namespace Tessa.Test.Server.Samples
{
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных SQL Server.
/// </summary>
[SetupTempDb(Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)]
public sealed class ExamplesTestMsSql :
ExamplesTest
{
}
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных PostgreSQL.
/// </summary>
[SetupTempDb(Dbms.PostgreSql, TestHelper.TempConfigurationStringPg, TestHelper.DbScriptDefaultPg)]
public sealed class ExamplesTestPostgreSql :
ExamplesTest
{
}
/// <summary>
/// Базовый абстрактный класс с примерами тестов.
/// </summary>
[Parallelizable, Category("Samples")]
public abstract class ExamplesTest :
KrServerTestBase
{
#region Test methods
/// <summary>
/// Проверят возможность сохранения карточки в зависимости от наличия темы документа.
/// </summary>
/// <param name="isModifySubject">Значение <see langword="true"/>, если тема документа указывается, иначе - <see langword="false"/>.</param>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task SaveCard(
[Values] bool isModifySubject)
{
var clc = await this.CreateCardLifecycleCompanion()
.Create()
.WithDocType(this.TestDocTypeID, this.TestDocTypeName)
.If(isModifySubject, c => c.ModifyDocument())
.GoAsync();
await clc
.Save()
.GoAsync((result) =>
{
// При наличии темы документа ожидается успешное выполнение, иначе - нет.
if (isModifySubject)
{
ValidationAssert.IsSuccessful(result);
}
else
{
ValidationAssert.HasErrors(
result,
new ValidationResultItemValidator(
ValidationResultType.Error,
CardValidationKeys.NullField)
//.CheckMessage() // Игнорируется при проверке. В данном случае проверку можно упростить из-за наличия ключевой информации в свойстве IValidationResultItem.ObjectName.
.CheckFieldName(null)
.CheckObjectName("DocumentCommonInfo.Subject")
.CheckObjectType("NotNullFieldValidator")
.CheckDetails(null));
}
});
}
/// <summary>
/// Проверяет работу регистрации и дерегистрации, реализуемую стандартными вторичными процессами: "Зарегистрировать документ" и "Отменить регистрацию".
/// </summary>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task RegisterDocument()
{
var clc = await this.CreateCardLifecycleCompanion()
.Create()
.WithDocType(this.TestDocTypeID, this.TestDocTypeName)
.ModifyDocument()
.Save()
.GoAsync();
await clc
.CreateKrProcess(KrConstants.RegisterButton)
.Load()
.ApplyAction((clc, _) => KrAssert.StateIs(clc, KrState.Registered))
.GoAsync();
await clc
.CreateKrProcess(KrConstants.DeregisterButton)
.Load()
.ApplyAction((clc, _) => KrAssert.StateIs(clc, KrState.Draft))
.GoAsync()
;
}
#endregion
#region Base overrides
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
await base.InitializeCoreAsync();
await this.TestConfigurationBuilder
.GetPermissionsConfigurator()
.GetPermissionsCard(Guid.NewGuid())
.AddType(this.TestDocTypeID)
.AddRole(this.Session.User.ID)
.ModifyStates(static _ => KrState.DefaultStates)
.AddFlags(KrPermissionFlagDescriptors.Full)
.Complete()
.GoAsync();
}
#endregion
}
}
Использование KrClientTestBase
¶
Пример использования KrClientTestBase
.
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Cards;
using Tessa.Extensions.Default.Shared.Workflow.KrPermissions;
using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
using Tessa.Platform.Data;
using Tessa.Platform.Validation;
using Tessa.Test.Default.Client.Kr;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.Kr;
namespace Tessa.Test.Client.Samples
{
[Parallelizable, Category("Samples")]
[SetupDbScope] /* (1) */
public sealed class ExamplesTest :
KrClientTestBase
{
#region Test methods
/// <summary>
/// Проверят возможность сохранения карточки в зависимости от наличия темы документа.
/// </summary>
/// <param name="isModifySubject">Значение <see langword="true"/>, если тема документа указывается, иначе - <see langword="false"/>.</param>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task SaveCard(
[Values] bool isModifySubject)
{
var clc = await this.CreateCardLifecycleCompanion()
.Create()
.WithDocType(this.TestDocTypeID, this.TestDocTypeName)
.If(isModifySubject, c => c.ModifyDocument())
.GoAsync();
this.TestCardManagerOnce.DeleteCardAfterTest(clc); /* (2) */
await clc
.Save()
.GoAsync((result) =>
{
// При наличии темы документа ожидается успешное выполнение, иначе - нет.
if (isModifySubject)
{
ValidationAssert.IsSuccessful(result);
}
else
{
ValidationAssert.HasErrors(
result,
new ValidationResultItemValidator(
ValidationResultType.Error,
CardValidationKeys.NullField)
//.CheckMessage() // Игнорируется при проверке. В данном случае проверку можно упростить из-за наличия ключевой информации в свойстве IValidationResultItem.ObjectName.
.CheckFieldName(null)
.CheckObjectName("DocumentCommonInfo.Subject")
.CheckObjectType("NotNullFieldValidator")
.CheckDetails(null));
}
});
}
/// <summary>
/// Проверяет работу регистрации и дерегистрации, реализуемую стандартными вторичными процессами: "Зарегистрировать документ" и "Отменить регистрацию".
/// </summary>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task RegisterDocument()
{
var clc = await this.CreateCardLifecycleCompanion()
.Create()
.WithDocType(this.TestDocTypeID, this.TestDocTypeName)
.ModifyDocument()
.Save()
.GoAsync();
this.TestCardManagerOnce.DeleteCardAfterTest(clc); /* (3) */
await clc
.CreateKrProcess(KrConstants.RegisterButton)
.Load()
.ApplyAction((clc, _) => KrAssert.StateIs(clc, KrState.Registered))
.GoAsync();
await clc
.CreateKrProcess(KrConstants.DeregisterButton)
.Load()
.ApplyAction((clc, _) => KrAssert.StateIs(clc, KrState.Draft))
.GoAsync()
;
}
#endregion
#region Base overrides
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
await base.InitializeCoreAsync();
await this.TestConfigurationBuilder
.GetPermissionsConfigurator()
.GetPermissionsCard(Guid.NewGuid())
.AddType(this.TestDocTypeID)
.AddRole(this.Session.User.ID)
.ModifyStates(static _ => KrState.DefaultStates)
.AddFlags(KrPermissionFlagDescriptors.Full)
.ModifyCard((configurator, c) =>
{
var currentKey = configurator.CurrentKey;
this.TestCardManagerOnce.DeleteCardAfterTest(c, (_, _) =>
{
configurator.Invalidate(currentKey);
return new ValueTask();
});
}) /* (4) */
.Complete()
.GoAsync();
}
#endregion
}
}
-
Используется подключение по умолчанию.
-
Удаление тестовой карточки, созданной в тесте
SaveCard
, после завершения всех тестов. -
Удаление тестовой карточки, созданной в тесте
RegisterDocument
, после завершения всех тестов. -
Удаление карточки правила доступа после завершения всех тестов.
Обратите внимание на отличия клиентского теста от серверного:
-
Используется подключение по умолчанию.
-
Удаление тестовой карточки, созданной в тесте
SaveCard
, после завершения всех тестов. -
Удаление тестовой карточки, созданной в тесте
RegisterDocument
, после завершения всех тестов. -
Удаление карточки правила доступа после завершения всех тестов.
Управление сессиями в клиентских тестах¶
По умолчанию клиентские тесты выполняются от имени пользователя, указанного в параметре UserName
файла конфигурации, если оно не переопределено в свойстве ClientTestBase.UserNameOverride
. Если оба значения равны null
, то используется аутентификация пользователя по учётной записи Windows.
Warning
Для одновременной работы от имени разных пользователей используемая лицензия должна разрешать достаточное количество конкурентных сессий. Используемая по умолчанию в тестах, выполняемых на временной базе данных, временная лицензия разрешает работу только одного пользователя. Для одновременной работы большего числа пользователей укажите действительный файл лицензии.
Note
При открытии новой сессии создаётся новый Unity-контейнер, который прозрачно заменяется в TestBase.UnityContainer
. После закрытия сессии значение восстанавливается на предыдущее.
Пример использования сессий.
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Extensions.Default.Shared;
using Tessa.Platform.Data;
using Tessa.Platform.Runtime;
using Tessa.Test.Default.Client.Kr;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.Roles;
namespace Tessa.Test.Client.Samples
{
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных SQL Server.
/// </summary>
[SetupTempDb(Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)]
public class SessionTestMsSql : SessionTest { }
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных PostgreSQL.
/// </summary>
[SetupTempDb(Dbms.PostgreSql, TestHelper.TempConfigurationStringPg, TestHelper.DbScriptDefaultPg)]
public class SessionTestPostgreSql : SessionTest { }
/// <summary>
/// Пример использования сессий в клиентских тестах.
/// </summary>
[Parallelizable, Category("Samples")]
public abstract class SessionTest :
KrHybridClientTestBase
{
#region Base Overrides
/// <inheritdoc/>
public override Guid TestCardTypeID => DefaultCardTypes.CarTypeID;
/// <inheritdoc/>
public override string TestCardTypeName => DefaultCardTypes.CarTypeName;
#endregion
#region Tests
/// <summary>
/// Тестирует создание карточки типа <see cref="TestCardTypeID"/> от имени разных пользователей.
/// </summary>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task Sessions()
{
// Unity-контейнер, содержащий зависимости для подключения от имени пользователя по умолчанию.
var defaultContainer = this.UnityContainer;
// Создание первого пользователя.
var otherUser = await TestRoleHelper.CreateUserAsync(
this.CardLifecycleDependencies,
static i => i.SetAccessLevel(UserAccessLevel.Administrator)); /* (1) */
// Открытие сессии от имени первого пользователя.
await using (await this.OpenSessionAsync(otherUser.GetAccount()))
{
// Unity-контейнер, содержащий зависимости для подключения от имени пользователя otherUser.
var otherContainer = this.UnityContainer;
// Создание карточки от имени первого пользователя.
var testCard = await this.CreateCardLifecycleCompanion()
.Create()
.GoAsync();
Assert.That(testCard.Card.ModifiedByID, Is.EqualTo(otherUser.CardID));
// Создание второго пользователя.
var otherUser2 = await TestRoleHelper.CreateUserAsync(
this.CardLifecycleDependencies);
// Открытие сессии от имени второго пользователя.
await using (await this.OpenSessionAsync(otherUser2.GetAccount()))
{
// Unity-контейнер, содержащий зависимости для подключения от имени пользователя otherUser2.
var other2Container = this.UnityContainer;
// Создание карточки от имени второго пользователя.
var testCard2 = await this.CreateCardLifecycleCompanion()
.Create()
.GoAsync();
Assert.That(testCard2.Card.ModifiedByID, Is.EqualTo(otherUser2.CardID));
}
}
// Создание карточки от имени пользователя по умолчанию.
var testCard3 = await this.CreateCardLifecycleCompanion()
.Create()
.GoAsync();
Assert.That(testCard3.Card.ModifiedByID, Is.EqualTo(this.Session.User.ID));
}
#endregion
}
}
- Уровень доступа “Администратор” необходим для создания второго пользователя от имени первого.
Использование KrHybridClientTestBase
¶
Пример использования KrHybridClientTestBase
.
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Cards;
using Tessa.Extensions.Default.Shared.Workflow.KrPermissions;
using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
using Tessa.Platform.Data;
using Tessa.Platform.Validation;
using Tessa.Test.Default.Client.Kr;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.Kr;
namespace Tessa.Test.Client.Samples
{
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных SQL Server.
/// </summary>
[SetupTempDb(Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)] /* (1) */
public sealed class ExamplesTestMsSql :
ExamplesTest
{
}
/// <summary>
/// Класс с примерами тестов, настроенный для выполнения на базе данных PostgreSQL.
/// </summary>
[SetupTempDb(Dbms.PostgreSql, TestHelper.TempConfigurationStringPg, TestHelper.DbScriptDefaultPg)]
public sealed class ExamplesTestPostgreSql :
ExamplesTest
{
}
[Parallelizable, Category("Samples")]
public abstract class ExamplesTest :
KrHybridClientTestBase /* (2) */
{
#region Test methods
/// <summary>
/// Проверят возможность сохранения карточки в зависимости от наличия темы документа.
/// </summary>
/// <param name="isModifySubject">Значение <see langword="true"/>, если тема документа указывается, иначе - <see langword="false"/>.</param>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task SaveCard(
[Values] bool isModifySubject)
{
var clc = await this.CreateCardLifecycleCompanion()
.Create()
.WithDocType(this.TestDocTypeID, this.TestDocTypeName)
.If(isModifySubject, c => c.ModifyDocument())
.GoAsync();
this.TestCardManagerOnce.DeleteCardAfterTest(clc);
await clc
.Save()
.GoAsync((result) =>
{
// При наличии темы документа ожидается успешное выполнение, иначе - нет.
if (isModifySubject)
{
ValidationAssert.IsSuccessful(result);
}
else
{
ValidationAssert.HasErrors(
result,
new ValidationResultItemValidator(
ValidationResultType.Error,
CardValidationKeys.NullField)
//.CheckMessage() // Игнорируется при проверке. В данном случае проверку можно упростить из-за наличия ключевой информации в свойстве IValidationResultItem.ObjectName.
.CheckFieldName(null)
.CheckObjectName("DocumentCommonInfo.Subject")
.CheckObjectType("NotNullFieldValidator")
.CheckDetails(null));
}
});
}
/// <summary>
/// Проверяет работу регистрации и дерегистрации, реализуемую стандартными вторичными процессами: "Зарегистрировать документ" и "Отменить регистрацию".
/// </summary>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task RegisterDocument()
{
var clc = await this.CreateCardLifecycleCompanion()
.Create()
.WithDocType(this.TestDocTypeID, this.TestDocTypeName)
.ModifyDocument()
.Save()
.GoAsync();
this.TestCardManagerOnce.DeleteCardAfterTest(clc);
await clc
.CreateKrProcess(KrConstants.RegisterButton)
.Load()
.ApplyAction((clc, _) => KrAssert.StateIs(clc, KrState.Registered))
.GoAsync();
await clc
.CreateKrProcess(KrConstants.DeregisterButton)
.Load()
.ApplyAction((clc, _) => KrAssert.StateIs(clc, KrState.Draft))
.GoAsync()
;
}
#endregion
#region Base overrides
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
await base.InitializeCoreAsync();
await this.TestConfigurationBuilder
.GetPermissionsConfigurator()
.GetPermissionsCard(Guid.NewGuid())
.AddType(this.TestDocTypeID)
.AddRole(this.Session.User.ID)
.ModifyStates(static _ => KrState.DefaultStates)
.AddFlags(KrPermissionFlagDescriptors.Full)
.Complete()
.GoAsync();
}
#endregion
}
}
-
Используется подключение к временной базе данных, как в серверном тесте.
-
Используется базовый класс
KrHybridClientTestBase
.
Обратите внимание на отличия клиентского теста, выполняемого на временной базе данных и специально созданном тестовом сервере, от простого клиентского теста:
-
Используется подключение к временной базе данных, как в серверном тесте.
-
Используется базовый класс
KrHybridClientTestBase
.
Использование WeScenarioTestBase
¶
Особенности использования:
-
Тесты WorkflowEngine являются разновидностью серверных тестов.
-
Для обеспечения возможности инициализации и проверки выполнения сценариев в основной скрипт процесса необходимо добавить следующий сценарий:
#region Test support members #region Constants
/// <summary> /// Строка в сообщении валидации, соответствующая успешному выполнению теста. /// </summary> private const string PassedStr = "Passed";
/// <summary> /// Строка в сообщении валидации, соответствующая ошибке при выполнении теста. /// </summary> private const string FailedStr = "Failed";
/// <summary> /// Имя ключа по которому в <see cref="CardInfoStorageObject.Info"/> карточки в которой запущен бизнес-процесс содержится значение флага, показывающего, запущен процесс из тестов или нет. Значение типа: <see cref="bool"/>. /// </summary> private const string IsLaunchedInTestKey = "IsLaunchedInTest";
/// <summary> /// Имя ключа, по которому в <see cref="CardInfoStorageObject.Info"/> карточки, в которой запущен бизнес-процесс, содержится метод инициализации бизнес-процесса при выполнении из тестов. Значение типа: <see cref="Func{T, TResult}"/>, где T - <see cref="WorkflowEngineCompiledBase"/>, TResult - <see cref="ValueTask"/>. /// </summary> private const string TestInitializerActionKey = "TestInitializerAction";
#endregion
#region Properties
/// <summary> /// Возвращает значение, показывающее, что процесс запущен из тестов. /// </summary> protected bool IsLaunchedInTest => this.ProcessHash.TryGet<bool>(IsLaunchedInTestKey);
#endregion
#region Protected Methods
/// <summary> /// Добавляет в результаты валидации сообщение, содержащее текст с признаком успешного выполнения теста "<see cref="PassedStr"/>[ <paramref name="suffix"/>]". /// </summary> /// <param name="suffix">Строка добавляемая к <see cref="PassedStr"/>.</param> /// <remarks>Не выполняет действий, если процесс выполняется не из тестов.</remarks> protected void Passed(string suffix = default) { if (!this.IsLaunchedInTest) { return; }
var str = PassedStr;
if (!string.IsNullOrEmpty(suffix)) { str += " " + suffix; }
this.AddInfo(str); }
/// <summary> /// Добавляет в результаты валидации сообщение типа <see cref="ValidationResultType.Error"/> с сообщением об успешном выполнении. /// </summary> /// <remarks> /// Созданное сообщение предназначено для остановки выполнения бизнес-процесса. Для этого при проверке результатов выполнения необходимо в методе <see cref="T:Tessa.Test.Default.Shared.Workflow.WeAssert.Passed"/> разрешить наличие ошибок в результате валидации.<para/> /// Не выполняет действий, если процесс выполняется не из тестов. /// </remarks> protected void PassedWithStop() { if (!this.IsLaunchedInTest) { return; }
this.AddError(PassedStr); }
/// <summary> /// Добавляет в результаты валидации сообщение, содержащее текст с признаком ошибки в тесте "<see cref="FailedStr"/>[ <paramref name="suffix"/>]". /// </summary> /// <param name="suffix">Строка добавляемая к <see cref="FailedStr"/>.</param> /// <remarks>Не выполняет действий, если процесс выполняется не из тестов.</remarks> protected void Failed(string suffix = default) { if (!this.IsLaunchedInTest) { return; }
this.AddError(FailedStr + " " + suffix); }
/// <summary> /// Инициализирует бизнес-процесс при выполнении из тестов. /// </summary> /// <returns>Асинхронная задача.<returns/> /// <remarks>Не выполняет действий, если процесс выполняется не из тестов.</remarks> protected async ValueTask InitProcessAsync() { var card = this.StoreCardObject;
if(card is null) { return; }
var info = card.TryGetInfo();
if (info != null && info.Remove(IsLaunchedInTestKey, out var isLaunchedInTestObj) && ((bool) isLaunchedInTestObj)) { this.ProcessHash.Add(IsLaunchedInTestKey, BooleanBoxes.True);
if (info.Remove(TestInitializerActionKey, out object testInitializerFuncAsyncObj)) { if(testInitializerFuncAsyncObj != null) { var initFuncAsync = (Func<WorkflowEngineCompiledBase, ValueTask>) testInitializerFuncAsyncObj; await initFuncAsync(this); } } } }
#endregion #endregion
Вспомогательные методы выполняются, только если в параметрах процесса указан параметр IsLaunchedInTest
со значением true
(устанавливается автоматически при запуске из тестов).
Пример использования WeScenarioTestBase
.
В примере тестируется следующий бизнес-процесс:
Параметры процесса:
-
IsIncrementCycle
.Тип данных: Да/Нет.
Значение по умолчанию:
false
. -
Author
Тип данных: Объект (хеш-таблица).
Тип объекта: Роль.
Значение по умолчанию: не задано.
-
Role
Тип данных: Объект (хеш-таблица).
Тип объекта: Роль.
Значение по умолчанию: не задано.
Параметры действий:
-
Действие “Старт процесса”.
-
Заголовок: KrAmendingAction.
-
Сценарий предобработки действия содержит:
this.InitProcess();
-
Запускающий сигнал:
KrAmendingAction
-
Разрешено выполнять сценарий предобработки на любой сигнал.
В итоге должно получиться:
-
Действие “Доработка”
-
Параметр “Роль” имеет привязку к параметру процесса
Role
. -
Параметр “Автор” имеет привязку к параметру процесса
Author
. -
Параметр “Увеличить цикл” имеет привязку к параметру процесса
IsIncrementCycle
. -
Сценарий инициализации задания содержит:
this.Passed("Task initialization - Script");
-
Сценарий завершения задания содержит:
this.Passed("Task completion - Script");
-
В итоге должно получиться:
-
Действие “Сценарий”
- Сценарий:
this.Passed("Action completion - Final - Success");
В итоге должно получиться:
После создания шаблона бизнес-процесса его необходимо экспортировать и сохранить в папке Source\Tests\Tessa.Test.Server\Resources\Cards\Workflow\
. При выполнении тестов все карточки из данной папки автоматически импортируются в базу данных на которой выполняется тестирование.
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Extensions.Default.Shared;
using Tessa.Extensions.Default.Shared.Workflow.KrPermissions;
using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
using Tessa.Extensions.Default.Shared.Workflow.WorkflowEngine;
using Tessa.Platform;
using Tessa.Platform.Data;
using Tessa.Platform.Validation;
using Tessa.Roles;
using Tessa.Test.Default.Server.Workflow;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.Cards;
using Tessa.Test.Default.Shared.Kr;
using Tessa.Test.Default.Shared.Roles;
using Tessa.Test.Default.Shared.Workflow;
using Tessa.Workflow;
using Tessa.Workflow.Compilation;
using Tessa.Workflow.Helpful;
namespace Tessa.Test.Server.Workflow.Scenarios.KrRoutes
{
/// <summary>
/// Предоставляет пример теста бизнес-процесса, настроенный для выполнения на базе данных SQL Server.
/// </summary>
[SetupTempDbForCardTypes(Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)]
public sealed class KrAmendingActionTestMs : KrAmendingActionTest { }
/// <summary>
/// Предоставляет пример теста бизнес-процесса, настроенный для выполнения на базе данных PostgreSQL.
/// </summary>
[SetupTempDbForCardTypes(Dbms.PostgreSql, TestHelper.TempConfigurationStringPg, TestHelper.DbScriptDefaultPg)]
public sealed class KrAmendingActionTestPg : KrAmendingActionTest { }
/// <summary>
/// Предоставляет пример теста бизнес-процесса.
/// </summary>
[Category("Samples")]
public abstract class KrAmendingActionTest : WeScenarioTestBase
{
#region Constants
/// <summary>
/// Имя сигнала отправляемого в бизнес-процесс.
/// </summary>
private const string KrAmendingActionName = "KrAmendingAction";
/// <summary>
/// Имя ключа в параметрах бизнес-процесса, к которому привязан параметр "Увеличить цикл" действия "Доработка".
/// </summary>
private const string IsIncrementCycle = nameof(IsIncrementCycle);
/// <summary>
/// Имя ключа в параметрах бизнес-процесса, к которому привязан параметр "Роль" действия "Доработка".
/// </summary>
private const string Role = nameof(Role);
/// <summary>
/// Имя ключа в параметрах бизнес-процесса, к которому привязан параметр "Автор" действия "Доработка".
/// </summary>
private const string Author = nameof(Author);
#endregion
#region Fields
private PersonalRole roleAuthor;
#endregion
#region Properties
/// <summary>
/// Возвращает идентификатор карточки шаблона процесса, используемого при тестировании.
/// </summary>
protected override Guid ProcessTemplateID => new Guid(0x5bc972df, 0x9f91, 0x467d, 0x81, 0x40, 0x08, 0x31, 0xdc, 0xa3, 0x9a, 0x14);
#endregion
#region Test
/// <summary>
/// Выполняет тестирование работы параметра "Увеличить цикл" действия "Доработка".
/// </summary>
/// <param name="isIncrementCycle">Значение <see langword="true"/>, если номер цикла согласования должен быть увеличен, иначе - <see langword="false"/>.</param>
/// <returns>Асинхронная задача.</returns>
[Test]
public async Task KrAmendingAction(
[Values] bool isIncrementCycle)
{
await this.ScenarioTestAsync(
KrAmendingActionName,
(clc, validationResult) => this.KrAmendingActionTestAsync(clc, validationResult, isIncrementCycle),
clc => this.KrAmendingActionInitTestAsync(clc, isIncrementCycle),
isProcessAlive: true); /* (1) */
}
#endregion
#region Test actions
/// <summary>
/// Инициализирует параметры тестируемого экземпляра бизнес-процесса.
/// </summary>
/// <param name="process">Объект компиляции, выполняемый при обработке процесса в <see cref="IWorkflowEngineProcessor"/>.</param>
/// <param name="isIncrementCycle">Значение <see langword="true"/>, если номер цикла согласования должен быть увеличен, иначе - <see langword="false"/>.</param>
/// <returns>Асинхронная задача.</returns>
private ValueTask KrAmendingActionInitTestAsync(
WorkflowEngineCompiledBase process,
bool isIncrementCycle)
{
WorkflowEngineHelper.Set(process.ProcessHash, this.Session.User.ID, Role, "ID");
WorkflowEngineHelper.Set(process.ProcessHash, this.Session.User.Name, Role, "Name");
WorkflowEngineHelper.Set(process.ProcessHash, this.roleAuthor.ID, Author, "ID");
WorkflowEngineHelper.Set(process.ProcessHash, this.roleAuthor.Name, Author, "Name");
WorkflowEngineHelper.Set(process.ProcessHash, BooleanBoxes.Box(isIncrementCycle), IsIncrementCycle);
return new ValueTask();
}
/// <summary>
/// Проверяет результат выполнения бизнес-процесса.
/// </summary>
/// <param name="clc">Объект, управляющий жизненным циклом карточки, в которой запущен экземпляр бизнес-процесса.</param>
/// <param name="result">Результат валидации.</param>
/// <param name="isIncrementCycle">Значение <see langword="true"/>, если номер цикла согласования должен быть увеличен, иначе - <see langword="false"/>.</param>
/// <returns>Асинхронная задача.</returns>
private async ValueTask KrAmendingActionTestAsync(
WeProcessInstanceLifecycleCompanion clc,
ValidationResult result,
bool isIncrementCycle)
{
result
.Passed("Скрипт инициализации задания.", "Task initialization - Script", expectedCount: 1); /* (2) */
KrAssert.HasTask(clc, DefaultTaskTypes.KrEditTypeID, 1); /* (3) */
KrAssert.StateIs(clc, KrState.Editing); /* (4) */
await clc
.GetTaskOrThrow(DefaultTaskTypes.KrEditTypeID, out var task)
.CompleteTask(task, DefaultCompletionOptions.NewApprovalCycle)
.GoAsync(validationResult =>
validationResult
.Passed("Скрипт завершения задания.", "Task completion - Script", expectedCount: 1)
.Passed("Выполнение перехода после завершения действия.", "Action completion - Final - Success", expectedCount: 1)); /* (5) */
var expectedCycle = isIncrementCycle ? 2 : 1;
(var processStorage, var processValidationResult) = await clc.GetProcessInstanceAsync(); /* (6) */
ValidationAssert.IsSuccessful(processValidationResult);
Assert.AreEqual(expectedCycle, WorkflowHelper.GetProcessCycle(processStorage.Hash)); /* (7) */
}
#endregion
#region SetUp
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
await base.InitializeCoreAsync();
this.roleAuthor = await TestRoleHelper.CreateUserAsync(this.RoleRepository);
await this.TestConfigurationBuilder
.GetPermissionsConfigurator()
.GetPermissionsCard(Guid.NewGuid())
.AddFlags(KrPermissionFlagDescriptors.Full)
.AddRole(this.Session.User.ID)
.AddRole(this.roleAuthor.ID)
.ModifyStates(static _ => KrState.DefaultStates)
.AddType(this.TestCardTypeID)
.Complete()
.GoAsync()
;
}
#endregion
}
}
-
В данном тесте после выполнения действий процесс не завершается для проверки контролируемого значения (номер цикла согласования), хранящегося в параметрах процесса.
-
Проверка выполнения скрипта инициализации задания при создании задания.
-
Проверка наличия указанного числа заданий типа “Доработка”.
-
Проверка состояния карточки.
-
Завершение задания и проверка действий, выполняющихся при этом: выполнение скрипта завершения задания и выполнение перехода на действие “Сценарий”.
Если необходимо завершить задание или выполнить действие от имени другого пользователя, то его необходимо разместить в блоке
await using (SessionContext.Create(new SessionToken(roleID, roleName)))
, гдеroleID
иroleName
идентификатор и имя пользователя, от имени которого должно быть выполнено действие соответственно.Пример завершения задания от имени другого пользователя
await using (SessionContext.Create(new SessionToken(roleID, roleName))) { await clc .GetTaskOrThrow(DefaultTaskTypes.KrEditTypeID, out var task) .CompleteTask(task, DefaultCompletionOptions.NewApprovalCycle) .GoAsync()); }
Note
Для выполнения отложенного действия (действия, выполняемого только при вызове метода
IPendingActionsExecutor.GoAsync(Action{ValidationResult}, CancellationToken)
) от имени другого пользователя, вызов этого метода должен быть расположен в области, в которой определена сессия пользователя, от имени которого должно быть выполнено запланированное действие.Пример выполнения отложенных действий от имени другого пользователя
clc .GetTaskOrThrow(DefaultTaskTypes.KrEditTypeID, out var task) // Получение задания заданного типа. .CompleteTask(task, DefaultCompletionOptions.NewApprovalCycle); // Планирование завершения задания.
await using (SessionContext.Create(new SessionToken(roleID, roleName))) { // Выполнение запланированных действий от имени пользователя с идентификатором - `roleID` и именем - `roleName`. await clc.GoAsync(); }
-
Получение экземпляра процесса.
-
Проверка значения параметра процесса, содержащего номер цикла согласования.
Выноски:
-
В данном тесте после выполнения действий процесс не завершается для проверки контролируемого значения (номер цикла согласования), хранящегося в параметрах процесса.
-
Проверка выполнения скрипта инициализации задания при создании задания.
-
Проверка наличия указанного числа заданий типа “Доработка”.
-
Проверка состояния карточки.
-
Завершение задания и проверка действий, выполняющихся при этом: выполнение скрипта завершения задания и выполнение перехода на действие “Сценарий”.
Если необходимо завершить задание или выполнить действие от имени другого пользователя, то его необходимо разместить в блоке
await using (SessionContext.Create(new SessionToken(roleID, roleName)))
, гдеroleID
иroleName
идентификатор и имя пользователя, от имени которого должно быть выполнено действие соответственно.Пример завершения задания от имени другого пользователя
await using (SessionContext.Create(new SessionToken(roleID, roleName))) { await clc .GetTaskOrThrow(DefaultTaskTypes.KrEditTypeID, out var task) .CompleteTask(task, DefaultCompletionOptions.NewApprovalCycle) .GoAsync()); }
Note
Для выполнения отложенного действия (действия, выполняемого только при вызове метода
IPendingActionsExecutor.GoAsync(Action{ValidationResult}, CancellationToken)
) от имени другого пользователя, вызов этого метода должен быть расположен в области, в которой определена сессия пользователя, от имени которого должно быть выполнено запланированное действие.Пример выполнения отложенных действий от имени другого пользователя
clc .GetTaskOrThrow(DefaultTaskTypes.KrEditTypeID, out var task) // Получение задания заданного типа. .CompleteTask(task, DefaultCompletionOptions.NewApprovalCycle); // Планирование завершения задания.
await using (SessionContext.Create(new SessionToken(roleID, roleName))) { // Выполнение запланированных действий от имени пользователя с идентификатором - `roleID` и именем - `roleName`. await clc.GoAsync(); }
-
Получение экземпляра процесса.
-
Проверка значения параметра процесса, содержащего номер цикла согласования.
Tip
Процессы WorkflowEngine могут быть запущены не в рамках классов, являющихся наследниками класса WeScenarioTestBase
, но они предоставляют базовую функциональность, упрощающую написание тестов. Вся логика инициализации экземпляра бизнес-процесса содержится в методе WeScenarioTestBase.ProcessScenarioCoreAsync
.
Настройка объектов тестовой базы данных¶
Общие сведения¶
Для упрощения настройки объектов тестовой базы данных после её инициализации предназначен класс Tessa.Test.Default.Shared.TestConfigurationBuilder
, предоставляющий методы для выполнения стандартных действий по настройке базы данных, импорту типов и карточек, настройке календаря, а также методы, возвращающие специализированные конфигураторы, например, выполняющие настройку типового решения, типов документов и др.
TestConfigurationBuilder
предоставляет следующие методы:
-
Методы, выполняющие настройку тестовой базы данных:
-
ConfigureCalendar
– настраивает календарь; -
BuildCalendar
– выполняет построение календаря; -
ImportCardsFromDirectory
– импортирует все карточки из ресурсов текущей сборки, расположенные в заданной директории; -
ImportCardsWithCardLib
– импортирует все карточки из ресурсов указанной сборки, описанные в файле библиотеки карточек (*.cardlib); -
ImportTypesFromDirectory
– импортирует все типы карточек из ресурсов указанной сборки, расположенные в заданной директории; -
ImportViewsFromDirectory
– импортирует все представления из ресурсов указанной сборки, расположенные в заданной директории; -
ExecuteSqlScripts
– выполняет указанную коллекцию SQL-скриптов, расположенных во встроенных ресурсах сборки по пути Resources\Sql. -
CardMetadataCacheInvalidate
– сбрасывает кэш с метаинформацией по карточкам;
-
Для выполнения запланированных действий вызовите метод IPendingActionsExecutor<T>.GoAsync(Action<ValidationResult>, CancellationToken)
.
Все стандартные конфигураторы можно использовать как самостоятельно, так и получать из конфигуратора верхнего уровня.
Note
Дополнительные, статические методы расширения расположены в классе Tessa.Test.Default.Shared.TestConfigurationBuilderExtensions
. Например, здесь содержатся методы:
-
Импортирующие карточки из стандартных расположений:
-
ImportKrProcessCards
– импортирует карточки типового процесса согласования, расположенные в папке “Resources\Cards\KrProcess”; -
ImportRoleCards
– импортирует карточки ролей, расположенные в папке “Resources\Cards\Roles\<SqlServer | PostgreSql>“; -
ImportNotificationCards
– импортирует карточки уведомлений (из папки “Resources\Cards\Notifications”) и типов уведомлений (из папки “NotificationTypes”); -
ImportSettingsCards
– импортирует карточки настроек (из папки “Resources\Cards\Settings”); -
ImportDocumentTypesCards
– импортирует карточки типов документов (из папки “Resources\Cards\DocumentTypes”).
-
-
Импортирующие карточки с учётом *.cardlib:
-
ImportCardsWithTessaCardLib
– импортирует карточки в соответствии с используемой СУБД: “Resources\Cards\Configuration\Tessa_(ms | pg).cardlib”; -
ImportCardsWithFileTemplatesCardLib
– импортирует карточки в соответствии с “Resources\Cards\Configuration\File templates.cardlib”.
-
-
Импортирующие типы карточек из стандартных расположений:
ImportAllTypes
– импортирует все типы карточек, расположенные в директориях: Resources\Types(Cards | Dialogs | Files | Tasks).
-
Методы, возвращающие конфигураторы:
-
GetServerConfigurator
– возвращает конфигуратор, предоставляющий методы, выполняющие настройку параметров сервера; -
GetPermissionsConfigurator
– возвращает конфигуратор, предоставляющий методы, выполняющие настройку правил доступа; -
GetLicenseConfigurator
– возвращает конфигуратор, предоставляющий методы, выполняющие настройку лицензий; -
GetKrSettingsConfigurator
– возвращает конфигуратор, предоставляющий методы, выполняющие настройку типового решения; -
GetKrDocTypesConfigurator
– возвращает конфигуратор, предоставляющий методы, выполняющие настройку типов документов.
-
Стандартные конфигураторы
Имя класса конфигуратора |
Описание |
---|---|
Tessa.Test.Default.Shared.Kr.KrDocTypesConfigurator | Предоставляет методы, выполняющие настройку типов документов |
Tessa.Test.Default.Shared.Kr.KrSettingsConfigurator | Предоставляет методы, выполняющие настройку параметров типового решения |
Tessa.Test.Default.Shared.Kr.LicenseConfigurator | Предоставляет методы, выполняющие настройку лицензий |
Tessa.Test.Default.Shared.Kr.PermissionsConfigurator | Предоставляет методы, выполняющие настройку правил доступа |
Tessa.Test.Default.Shared.Kr.ServerConfigurator | Предоставляет методы, выполняющие настройку параметров сервера |
Пример использования конфигураторов
/// <summary>
/// Возвращает идентификатор типа карточки, используемой в тестах.
/// </summary>
protected virtual Guid TestCardTypeID => DefaultCardTypes.DocumentTypeID;
/// <summary>
/// Возвращает имя типа карточки, используемой в тестах.
/// </summary>
protected virtual string TestCardTypeName => DefaultCardTypes.DocumentTypeName;
/// <summary>
/// Возвращает идентификатор типа документа, используемого в тестах.
/// </summary>
protected virtual Guid TestDocTypeID => new Guid(0x93A392E7, 0x097C, 0x4420, 0x85, 0xC4, 0xDB, 0x10, 0xB2, 0xDF, 0x3C, 0x1D);
/// <summary>
/// Возвращает имя типа документа, используемого в тестах.
/// </summary>
protected virtual string TestDocTypeName => this.TestCardTypeID.ToString();
/// <summary>
/// Возвращает текущую сессию.
/// </summary>
protected ISession Session { get; }
/// <summary>
/// Возвращает объект, управляющий удалением карточек после завершения теста.
/// </summary>
protected ITestCardManager TestCardManager { get; }
/// <summary>
/// Возвращает конфигуратор тестовой базы данных.
/// </summary>
protected TestConfigurationBuilder TestConfigurationBuilder { get; }
...
await this.TestConfigurationBuilder
.ImportAllTypes() /* (1) */
.ImportCardsWithTessaCardLib() /* (2) */
.ConfigureCalendar() /* (3) */
.GetServerConfigurator() /* (4) */
.CreateOrLoadSingleton() /* (5) */
.InitializeServerInstance(await this.GetFileStoragePathAsync()) /* (6) */
.Save() /* (7) */
.Complete() /* (8) */
.GetKrDocTypesConfigurator() /* (9) */
.GetDocTypeCard(
this.TestDocTypeID,
cardTypeID: this.TestCardTypeID) /* (10) */
.UseApproving() /* (11) */
.UseRegistration() /* (12) */
.UseResolutions() /* (13) */
.Complete() /* (14) */
.GetKrSettingsConfigurator() /* (15) */
.CreateOrLoadSingleton() /* (16) */
.GetCardTypeConfigurator(this.TestCardTypeID) /* (17) */
.UseDocTypes() /* (18) */
.Complete() /* (19) */
.Complete() /* (20) */
.GetLicenseConfigurator() /* (21) */
.CreateOrLoadSingleton() /* (22) */
.WithMobileUser(this.Session.User.ID, this.Session.User.Name) /* (23) */
.Complete() /* (24) */
.GetPermissionsConfigurator() /* (25) */
.GetPermissionsCard(Guid.NewGuid()) /* (26) */
.AddFlags(KrPermissionFlagDescriptors.Full) /* (27) */
.AddRole(this.Session.User.ID) /* (28) */
.AddType(this.TestCardTypeID) /* (29) */
.AddState(KrState.Active) /* (30) */
.AddState(KrState.Approved)
.ModifyCard((configurator, clc) =>
{
var currentKey = configurator.CurrentKey;
this.TestCardManager.DeleteCardAfterTest(
clc,
(clc, calcellationToken) =>
{
configurator.Invalidate(currentKey);
return new ValueTask();
});
}) /* (31) */
.Complete() /* (32) */
.GoAsync() /* (33) */
;
-
Планирование импорта всех типов карточек.
-
Планирование импорта карточек в соответствии с
Tessa_(ms | pg).cardlib
. -
Планирование настройки календаря.
-
Получение объекта конфигуратора
ServerConfigurator
, выполняющего настройку сервера. -
Планирование создания или получения карточки настроек сервера. Если карточки нет в базе данных, то она будет создана.
Если вызов конфигуратора выполняется в клиентских тестах и карточка отсутствует в базе данных, то следует использовать метод
Create()
. -
Планирование инициализации карточки настроек сервера.
-
Планирование сохранения карточки настроек сервера, инициализированной в предыдущем пункте.
-
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
. -
Получение объекта конфигуратора
KrDocTypesConfigurator
типов документов. -
Установка в качестве настраиваемой карточки типа документа карточки с идентификатором
this.TestDocTypeID
. Если карточка не загружена в кэш конфигуратора, то она будет создана. Для загрузки существующей карточки из базы данных необходимо задать значениеtrue
параметруisLoad
. Если тип документа создаётся, то должен быть указан также и идентификатор типа карточки. В примере это –this.TestCardTypeID
. -
Планирование разрешения использования согласования для настраиваемого типа документа.
-
Планирование разрешения использования регистрации для настраиваемого типа документа.
-
Планирование разрешения использования типового процесса отправки задач для настраиваемого типа документа.
-
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
. -
Получение объекта конфигуратора
KrSettingsConfigurator
, выполняющего настройку типового решения. -
Инициализация конфигуратора – получение карточки типового решения. Если карточки нет в базе данных, то она будет создана.
-
Получение объекта конфигуратора
KrSettingsTypeConfigurator
, выполняющего настройку типа карточки, имеющего идентификатор, возвращаемый свойствомthis.TestCardTypeID
. -
Планирование разрешения использования типов документов для карточки настраиваемого типа.
-
Возврат к конфигуратору верхнего уровня –
KrSettingsConfigurator
. -
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
. -
Получение объекта конфигуратора
LicenseConfigurator
, выполняющего настройку лицензий. -
Инициализация конфигуратора – получение карточки лицензий. Если карточки нет в базе данных, то она будет создана.
-
Планирование добавления указанного пользователя в список “Мобильное согласование” в карточке “Лицензия”.
-
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
. -
Получение объекта конфигуратора
PermissionsConfigurator
для настройки правил доступа. -
Установка в качестве настраиваемой карточки, карточки правила доступа с заданным идентификатором. Если карточка с указанным идентификатором отсутствует в кэше конфигуратора, то она будет создана. Для загрузки существующей карточки из базы данных необходимо задать значение
true
параметруisLoad
. -
Планирование задания всех разрешений.
-
Планирование изменения списка ролей, для которых выдаются указанные права для указанных типов карточек в указанных состояниях.
Если необходимо, например, добавить новую роль к существующему списку ролей, то можно объединить старый и новый списки следующим образом:
.ModifyRoles(p => p.Concat(new[] { this.Session.User.ID }))
Для использования метода
Concat
требуется подключить пространство имёнSystem.Linq
. -
Планирование изменения списка типов карточек, к которым применяется правило доступа.
-
Планирование изменения списка состояний карточки, в которых будут работать определённые разрешения.
-
Изменение карточки правила доступа. Планирование удаления карточки правила доступа после завершения теста. В серверных тестах данное действие обычно не требуется выполнять, кроме случаев, когда тестирование выполняется на постоянной базе данных.
Для управления удалением карточек после выполнения тестов предназначены объекты, предоставляемые свойствами
TestCardManager
иTestCardManagerOnce
классаTessa.Test.Default.Shared.TestBase
. -
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
.Warning
Обратите внимание: если не выполнить возврат к конфигуратору верхнего уровня, то при выполнении запланированных действий будут выполнены действия, запланированные в текущем конфигураторе. В данном случае –
PermissionsConfigurator
. Действия, запланированные в других конфигураторах, выполнены не будут. -
Выполнение запланированных действий и проверка результата выполнения.
Выноски:
-
Планирование импорта всех типов карточек.
-
Планирование импорта карточек в соответствии с
Tessa_(ms | pg).cardlib
. -
Планирование настройки календаря.
-
Получение объекта конфигуратора
ServerConfigurator
, выполняющего настройку сервера. -
Планирование создания или получения карточки настроек сервера. Если карточки нет в базе данных, то она будет создана.
Если вызов конфигуратора выполняется в клиентских тестах и карточка отсутствует в базе данных, то следует использовать метод
Create()
. -
Планирование инициализации карточки настроек сервера.
-
Планирование сохранения карточки настроек сервера, инициализированной в предыдущем пункте.
-
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
. -
Получение объекта конфигуратора
KrDocTypesConfigurator
типов документов. -
Установка в качестве настраиваемой карточки типа документа карточки с идентификатором
this.TestDocTypeID
. Если карточка не загружена в кэш конфигуратора, то она будет создана. Для загрузки существующей карточки из базы данных необходимо задать значениеtrue
параметруisLoad
. Если тип документа создаётся, то должен быть указан также и идентификатор типа карточки. В примере это –this.TestCardTypeID
. -
Планирование разрешения использования согласования для настраиваемого типа документа.
-
Планирование разрешения использования регистрации для настраиваемого типа документа.
-
Планирование разрешения использования типового процесса отправки задач для настраиваемого типа документа.
-
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
. -
Получение объекта конфигуратора
KrSettingsConfigurator
, выполняющего настройку типового решения. -
Инициализация конфигуратора – получение карточки типового решения. Если карточки нет в базе данных, то она будет создана.
-
Получение объекта конфигуратора
KrSettingsTypeConfigurator
, выполняющего настройку типа карточки, имеющего идентификатор, возвращаемый свойствомthis.TestCardTypeID
. -
Планирование разрешения использования типов документов для карточки настраиваемого типа.
-
Возврат к конфигуратору верхнего уровня –
KrSettingsConfigurator
. -
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
. -
Получение объекта конфигуратора
LicenseConfigurator
, выполняющего настройку лицензий. -
Инициализация конфигуратора – получение карточки лицензий. Если карточки нет в базе данных, то она будет создана.
-
Планирование добавления указанного пользователя в список “Мобильное согласование” в карточке “Лицензия”.
-
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
. -
Получение объекта конфигуратора
PermissionsConfigurator
для настройки правил доступа. -
Установка в качестве настраиваемой карточки, карточки правила доступа с заданным идентификатором. Если карточка с указанным идентификатором отсутствует в кэше конфигуратора, то она будет создана. Для загрузки существующей карточки из базы данных необходимо задать значение
true
параметруisLoad
. -
Планирование задания всех разрешений.
-
Планирование изменения списка ролей, для которых выдаются указанные права для указанных типов карточек в указанных состояниях.
Если необходимо, например, добавить новую роль к существующему списку ролей, то можно объединить старый и новый списки следующим образом:
.ModifyRoles(p => p.Concat(new[] { this.Session.User.ID }))
Для использования метода
Concat
требуется подключить пространство имёнSystem.Linq
. -
Планирование изменения списка типов карточек, к которым применяется правило доступа.
-
Планирование изменения списка состояний карточки, в которых будут работать определённые разрешения.
-
Изменение карточки правила доступа. Планирование удаления карточки правила доступа после завершения теста. В серверных тестах данное действие обычно не требуется выполнять, кроме случаев, когда тестирование выполняется на постоянной базе данных.
Для управления удалением карточек после выполнения тестов предназначены объекты, предоставляемые свойствами
TestCardManager
иTestCardManagerOnce
классаTessa.Test.Default.Shared.TestBase
. -
Возврат к конфигуратору верхнего уровня –
TestConfigurationBuilder
.Warning
Обратите внимание: если не выполнить возврат к конфигуратору верхнего уровня, то при выполнении запланированных действий будут выполнены действия, запланированные в текущем конфигураторе. В данном случае –
PermissionsConfigurator
. Действия, запланированные в других конфигураторах, выполнены не будут. -
Выполнение запланированных действий и проверка результата выполнения.
Настройка импорта карточек в тестовую базу данных¶
При использовании базовых классов KrServerTestBase
или KrHybridClientTestBase
импорт карточек выполняется в методе IImportObjects.ImportCardsAsync
. При необходимости данный метод можно переопределить и, например, реализовать импорт карточек из другого расположения или с использованием нестандартных файлов библиотек карточек. Для настройки импорта карточек используются следующие члены, описываемые в IImportObjects
:
IsImportCards
– возвращает значение, показывающее, необходимо ли выполнять импорт карточек из конфигурации.IsImportFileTemplateCards
– возвращает значение, показывающее, необходимо ли выполнять импорт карточек шаблонов файлов из конфигурации.ImportCardsAsync
– импортирует карточки в тестовую базу данных.ImportCardPredicateAsync
– фильтрует импортируемые карточки. Используется, если свойствоIsImportCards
имеет значениеtrue
.ImportFileTemplateCardPredicateAsync
– фильтрует импортируемые карточки шаблонов файлов. Используется, если свойствоIsImportFileTemplateCards
имеет значениеtrue
.
Рассмотрим типовую реализацию в классе KrServerTestBase
(в KrHybridClientTestBase
аналогичная):
/// <inheritdoc/>
public virtual bool IsImportCards => true; /* (1) */
/// <inheritdoc/>
public virtual bool IsImportFileTemplateCards => true; /* (2) */
/// <inheritdoc/>
public virtual async Task ImportCardsAsync()
{
await this.TestConfigurationBuilder
.GetServerConfigurator()
.Create()
.InitializeServerInstance(await this.GetFileStoragePathAsync())
.Save()
.GoAsync(); /* (3) */
await this.TestConfigurationBuilder
.If(this.IsImportCards,
c => c.ImportCardsWithTessaCardLib(this.ImportCardPredicateAsync)) /* (4) */
.If(this.IsImportFileTemplateCards,
c => c.ImportCardsWithFileTemplatesCardLib(this.ImportFileTemplateCardPredicateAsync)) /* (5) */
.GoAsync()
;
}
/// <inheritdoc/>
public virtual ValueTask<bool> ImportCardPredicateAsync(
Card card,
CancellationToken cancellationToken = default) =>
new ValueTask<bool>(card.TypeID != CardHelper.ServerInstanceTypeID); /* (6) */
/// <inheritdoc/>
public virtual ValueTask<bool> ImportFileTemplateCardPredicateAsync(
Card card,
CancellationToken cancellationToken = default) =>
new ValueTask<bool>(true);
- Флаг, разрешающий импорт карточек.
- Флаг, разрешающий импорт файловых шаблонов.
-
Создание карточки настроек сервера по умолчанию для использования в тестах. Карточка настроек сервера, создаваемая для использования в тестах, имеет отличие только в используемом хранилище по умолчанию: FileSystem (ID = 2), местоположение равно значению, возвращаемому методом
TestHelper.GetFileStoragePath
. Более подробно про настройку файлового хранилища в тестах читайте в следующем разделе. -
Планирование импорта карточек в соответствии с библиотекой карточек
Tessa_(ms | pg).cardlib
, если свойствоIsImportCards
имеет значениеtrue
. Для фильтрации импортируемых карточек используется методImportCardPredicateAsync
. - Планирование импорта карточек в соответствии с библиотекой карточек
File templates.cardlib
, если свойствоIsImportFileTemplateCards
имеет значениеtrue
. Для фильтрации импортируемых карточек используется методImportFileTemplateCardPredicateAsync
. - При импорте карточек исключаются карточки, соответствующие типу “Настройки сервера” для того, чтобы они не перезаписали карточку, созданную в п. 3.
Выноски:
- Флаг, разрешающий импорт карточек.
- Флаг, разрешающий импорт файловых шаблонов.
-
Создание карточки настроек сервера по умолчанию для использования в тестах. Карточка настроек сервера, создаваемая для использования в тестах, имеет отличие только в используемом хранилище по умолчанию: FileSystem (ID = 2), местоположение равно значению, возвращаемому методом
TestHelper.GetFileStoragePath
. Более подробно про настройку файлового хранилища в тестах читайте в следующем разделе. -
Планирование импорта карточек в соответствии с библиотекой карточек
Tessa_(ms | pg).cardlib
, если свойствоIsImportCards
имеет значениеtrue
. Для фильтрации импортируемых карточек используется методImportCardPredicateAsync
. - Планирование импорта карточек в соответствии с библиотекой карточек
File templates.cardlib
, если свойствоIsImportFileTemplateCards
имеет значениеtrue
. Для фильтрации импортируемых карточек используется методImportFileTemplateCardPredicateAsync
. - При импорте карточек исключаются карточки, соответствующие типу “Настройки сервера” для того, чтобы они не перезаписали карточку, созданную в п. 3.
Настройка файлового хранилища при создании тестовой базы данных¶
Перед выполнением любых действий необходимо правильно настроить файловые хранилища, используемые в тестах. Особенностью тестов является то, что они могут выполняться параллельно, т.о. необходимо обеспечить независимость файловых хранилищ, используемых в различных наборах тестов.
По умолчанию при инициализации созданной карточки настроек сервера с помощью метода ServerConfigurator.InitializeServerInstance
для файлового хранилища FileSystem (ID = 2), местоположение равно значению, заданному параметру fileStoragePath
.
Пример создания карточки настроек сервера с параметрами, рекомендуемыми для использования в тестах
await this.TestConfigurationBuilder
.GetServerConfigurator()
.Create()
.InitializeServerInstance(await this.GetFileStoragePathAsync())
.Save()
.GoAsync();
Пример изменения существующей или создание новой карточки настроек сервера, имеющей нестандартный путь к файловому хранилищу
// Импорт карточки настроек сервера.
// Не импортируйте карточки, содержащие файлы до настройки файловых хранилищ.
var clc = await this.TestConfigurationBuilder
.GetServerConfigurator()
.CreateOrLoadSingleton()
.InitializeServerInstance(await this.GetFileStoragePathAsync())
.GoAsync();
await clc
.ChangeFileSourcePathWithTestSource(
CardFileSourceType.FileSystem.ID,
_ => Path.Combine("C:\\Tessa\\Files\\Tests", await this.GetFixtureNameAsync()))
.Save()
.GoAsync();
// Импорт оставшихся карточек.
Note
В качестве переменной части пути к файловому хранилищу рекомендуется использовать значение, зависящее от класса, содержащего тесты. Это позволит просто определить путь к хранилищу при следующем выполнении теста и удалить его.
Рекомендуется использовать значение возвращаемое IFixtureNameProvider.GetFixtureNameAsync()
.
Временное файловое хранилище необходимо удалять перед и после выполнения тестов. Удаление “перед выполнением” необходимо для предотвращения возможного влияния файлов, оставшихся после предыдущего выполнения, если они не были корректно удалены после выполнения.
Управление подключением к базе данных¶
Управление подключением к базе данных на уровне класса¶
Управление подключением к базе данных, которое используется в тесте, осуществляется с помощью значений свойств, определяемых интерфейсом IDbScopeContainer
. Для упрощения их задания используется атрибут Tessa.Test.Default.Shared.SetupDbScopeAttribute
и его наследники.
Note
Для успешного применения атрибута класс должен реализовывать интерфейсы: Tessa.Test.Default.Shared.IDbScopeContainer
и Tessa.Test.Default.Shared.ITestActions
.
Note
Рекомендуется использовать базовые классы для тестов (класс Tessa.Test.Default.Shared.TestBase
и его наследники).
Note
Если значения свойств, определяемые интерфейсом IDbScopeContainer
, не заданы, то они инициализируются соответствующими значениями, полученными из Unity-контейнера (свойство TestBase.UnityContainer
), если он их содержит перед выполнением метода TestBase.InitializeCoreAsync
. В этом случае, при использовании типовых базовых классов ClientTestBase
или ServerTestBase
, подключение будет выполняться в соответствии со строкой подключения с именем “default”.
Пример использования атрибута SetupDbScopeAttribute
при использовании подключения по умолчанию – default
.
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Test.Default.Shared;
namespace Tessa.Test.Server.Samples
{
[SetupDbScope]
public sealed class DefaultConnectionTest :
ServerTestBase
{
#region Test Methods
[Test]
public async Task TestMethod()
{
await using (this.DbScope.Create())
{
// Работа с базой данных, указанной в строке подключения с именем "default".
}
}
#endregion
}
}
Для использования в тесте временной базы данных используется атрибут SetupTempDbAttribute
и его наследники.
При включённом параметре RandomizeDbName
(по умолчанию он включён) имя временной базы данных рандомизируется для возможности параллельного выполнения тестов.
Формат имени базы данных при включённом параметре RandomizeDbName
:
<имя базы данных, указанное в строке подключения>_<идентификатор класса, содержащего текущий тест>
Например, для теста, расположенного в классе Tessa.Test.Server.Samples.ConnectionTestMsSql
и имени базы данных tessa_test
, указанной в строке подключения для SQl Server, имя временной базы данных будет: tessa_test_2D229B1E
.
Note
При принудительной остановке выполнения теста, из-за которой не были выполнены действия, запланированные к выполнению после выполнения всех тестов текущего набора тестов, временная база данных не будет удалена автоматически. Её можно безопасно удалить вручную. Если этого не сделать, то она будет пересоздана при следующем запуске любого теста из набора тестов к которому относился остановленный тест.
Одной из причин, приводящей к указанному поведению, является прерывание выполнения тестов по кнопке “Отмена”, расположенной на панели обозревателя тестов IDE Microsoft Visual Studio.
Пример использования атрибута SetupTempDbAttribute
при использовании подключения по умолчанию, SQL Server и PostgreSQL к временной базе данных.
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Platform.Data;
using Tessa.Test.Default.Shared;
namespace Tessa.Test.Server.Samples
{
/// <summary>
/// Создание подключения к временной базе данных, начинающейся с имени базы данных, указанной в строке подключения по умолчанию и выполнение SQL-скрипта для создания объектов SQL по схеме для новой БД.
/// </summary>
[SetupTempDb(Dbms.SqlServer, TestHelper.DefaultConfigurationString, TestHelper.DbScriptDefaultMs)]
public sealed class ConnectionTestDefault : ConnectionTest { }
/// <summary>
/// Создание подключения к временной базе данных, начинающейся с имени базы данных, указанной в строке подключения SQl Server и выполнение SQL-скрипта для создания объектов SQL по схеме для новой БД.
/// </summary>
[SetupTempDb(Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)]
public sealed class ConnectionTestMsSql : ConnectionTest { }
/// <summary>
/// Создание подключения к временной базе данных, начинающейся с имени базы данных, указанной в строке подключения PostgreSql и выполнение SQL-скрипта для создания объектов SQL по схеме для новой БД.
/// </summary>
[SetupTempDb(Dbms.PostgreSql, TestHelper.TempConfigurationStringPg, TestHelper.DbScriptDefaultPg)]
public sealed class ConnectionTestPostgreSql : ConnectionTest { }
/// <summary>
/// Пример создания подключения к временной базе данных.
/// </summary>
public abstract class ConnectionTest :
ServerTestBase
{
#region Test Methods
[Test]
public async Task TestMethod()
{
await using (this.DbScope.Create())
{
// Работа с базой данных.
}
}
#endregion
}
}
Также существуют атрибуты Tessa.Test.Default.Shared.Cards.SetupTempDbForCardTypesAttribute
и Tessa.Test.Default.Shared.Roles.SetupTempDbForRolesAttribute
, позволяющие дополнительно настроить объекты Tessa.Cards.ICardTypeServerRepository
и Tessa.Roles.IRoleRepository
без необходимости создания и инициализации Unity контейнера соответственно.
При применении указанных в данном разделе атрибутов, тестовые методы автоматически помечаются категорией, соответствующей типу СУБД, строка подключения которой используется.
Категории, соответствующие типу СУБД
Тип СУБД (значение перечисления Tessa.Platform.Data.Dbms ) |
Категория |
---|---|
Dbms.Unknown | Отсутствует |
Dbms.SqlServer | db-ms |
Dbms.PostgreSql | db-pg |
Note
Категории, соответствующие типу СУБД, к которой выполняется подключение в тесте, могут не отображаться в Обозревателе тестов сразу после открытия решения. Для получения полных результатов о доступных тестах выполните сборку решения (см. Обнаружение динамических тестов).
Управление подключением к базе данных на уровне метода¶
Warning
В общем случае следует использовать вариант управления подключением к базе данных на уровне класса.
Описываемый в данном пункте подход предназначен для работы с базой данных без использования Tessa.Platform.Data.DbManager
.
Для управления используемым подключением на уровне метода предназначен атрибут Tessa.Test.Default.Shared.DatabaseTestAttribute
и его наследники.
-
Если имя строки подключения не задано, то используются все доступные строки подключения.
-
По умолчанию имя базы данных рандомизируется в зависимости от текущего теста, что позволяет выполнять тесты параллельно. Для управления рандомизацией используется свойство
DatabaseTestAttribute.RandomizeDbName
.Формат имени базы данных при включённом параметре
RandomizeDbName
:
<имя базы данных, указанное в строке подключения или имя метода теста, если имя базы данных не задано в строке подключения>_<идентификатор класса, содержащего текущий тест> -
Имя строки подключения может содержать только часть имени. Для этого необходимо указать имя строки подключения в виде:
<строка_с_которой_должно_начинаться_имя_строки_подключения>*
. -
При применении указанных в данном разделе атрибутов, тестовые методы автоматически помечаются категорией соответствующей типу СУБД, строка подключения к которой используется.
Пример использования атрибута DatabaseTestAttribute
для управления подключением и атрибута Tessa.Test.Default.Shared.DatabaseScriptsAttribute
для инициализации базы данных
using System;
using System.Data.Common;
using NUnit.Framework;
using Tessa.Test.Default.Shared;
namespace Tessa.Test.Server.Samples
{
public sealed class SampleConnectionTest
{
#region Test Methods
/// <summary>
/// Пример использования <see cref="DatabaseTestAttribute"/>.
/// Для всех строк подключение выполнить тестовый метод.
/// </summary>
/// <param name="connectionFactory">Фабрика подключений к базе данных.</param>
/// <returns>Асинхронная задача.</returns>
[Test]
[DatabaseTest()]
public void TestMethod(Func<DbConnection> connectionFactory)
{
}
#endregion
}
}
Для инициализации базы данных можно использовать атрибут Tessa.Test.Default.Shared.DatabaseScriptsAttribute
, позволяющий выполнять указанные SQL-скрипты перед выполнением теста.
Пример использования атрибута DatabaseTestAttribute
для управления подключением и атрибута DatabaseScriptsAttribute
для инициализации базы данных
using System;
using System.Data;
using System.Data.Common;
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Platform.Data;
using Tessa.Test.Default.Shared;
namespace Tessa.Test.Server.Samples
{
public sealed class SampleConnectionTest
{
#region Test Methods
/// <summary>
/// Пример использования <see cref="DatabaseTestAttribute"/>.
/// Для всех строк подключение, чьё имя начинается с "temp_", выполнить тестовый метод.
/// </summary>
/// <param name="connectionFactory">Фабрика подключений к базе данных. Опциональный параметр, может быть не задан.</param>
/// <returns>Асинхронная задача.</returns>
[Test]
[DatabaseTest(ConnectionString = "temp_*")]
// Скрипты выполняющие инициализацию базы данных.
[DatabaseScripts(Dbms.SqlServer, TestHelper.DbScriptEmptyMs)]
[DatabaseScripts(Dbms.PostgreSql, TestHelper.DbScriptEmptyPg)]
public async Task TestMethod(Func<DbConnection> connectionFactory)
{
await using var connection = connectionFactory();
// Открытие подключения.
if (connection.State == ConnectionState.Closed)
{
await connection.OpenAsync();
}
// Создание команды и запроса.
await using var command = connection.CreateCommand();
var dbms = connection.GetDbms();
var query = new QueryBuilderFactory(dbms)
.Select()
.Count()
.From("Instances")
.Build();
command.CommandText = query;
// Выполнение запроса.
var itemsCount = await command.ExecuteScalarAsync<int>();
// Выполнение проверки.
Assert.Zero(itemsCount);
}
#endregion
}
}
Области выполнения¶
Область выполнения – это область в которой тесты выполняются c использованием одних и тех же общих разделяемых ресурсов. Это могут быть: база данных, файловое хранилище, локальный кэш метаинформации и т.п.
Использование областей выполнения позволяет, например, создать базу данных и инициализировать её необходимыми данными. Это становится особенно полезно, когда инициализация ресурсов занимает существенное время.
Для использования областей выполнения в тестах к классу, содержащему тесты, необходимо применить атрибут TestScopeAttribute
. Параметр атрибута: имя области выполнения. Имя области выполнения – строка идентифицирующая область выполнения группы тестов (test fixture).
Note
Контекст текущей области выполнения содержится в KrTestContext.ScopeContext
.
Note
Вспомогательные методы для работы с областями выполнения расположены в классе Tessa.Test.Default.Shared.TestScopeHelper
.
Note
Области выполнения можно глобально включить или отключить с помощью параметра UseTestScope
. Это может быть полезно, например, для изолированного выполнения тестов.
При применении атрибута TestScopeAttribute
для тестов добавляется категория, соответствующая следующей строке: scope-<Имя_области_выполнения>
. К примеру, если имя области выполнения – Test
, тогда имя категории будет – scope-Test
.
Пример использования атрибута TestScopeAttribute
using System.Threading.Tasks;
using NUnit.Framework;
using Tessa.Platform.Data;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.Kr;
namespace Tessa.Test.Server
{
[SetupTempDb(Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)]
public class ExampleTestMsSql : ExampleTest { }
[SetupTempDb(Dbms.PostgreSql, TestHelper.TempConfigurationStringPg, TestHelper.DbScriptDefaultPg)]
public class ExampleTestPostgreSql : ExampleTest { }
[TestScope(TestScopeSqlServer)]
[SetupTempDb(Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)]
public class ExampleTestWithScopeTestMsSql : ExampleTest { }
[TestScope(TestScopePostgreSql)]
[SetupTempDb(Dbms.PostgreSql, TestHelper.TempConfigurationStringPg, TestHelper.DbScriptDefaultPg)]
public class ExampleTestWithScopeTestPostgreSql : ExampleTest { }
/// <summary>
/// Пример использования областей выполнения.
/// </summary>
[Parallelizable]
public abstract class ExampleTest :
KrServerTestBase
{
#region Constants And Static Fields
/// <summary>
/// Область выполнения по умолчанию для БД содержащей данные импортированные
/// в <see cref="ExampleTest"/> и SQL-скриптом <see cref="TestHelper.DbScriptDefaultMs"/>
/// под управлением Sql Server.
/// </summary>
protected const string TestScopeSqlServer = "ExampleTestScope-" + TestHelper.ShortSqlServerName;
/// <summary>
/// Область выполнения по умолчанию для БД содержащей данные импортированные
/// в <see cref="ExampleTest"/> и SQL-скриптом <see cref="TestHelper.DbScriptDefaultMs"/>
/// под управлением PostgreSql.
/// </summary>
protected const string TestScopePostgreSql = "ExampleTestScope-" + TestHelper.ShortPostgreSqlName;
#endregion
#region Tests
[Test]
public void TestMethod()
{
}
#endregion
#region Base Overrides
/// <inheritdoc/>
protected override async Task InitializeScopeCoreAsync()
{
await base.InitializeScopeCoreAsync();
// Действие выполняемое при инициализации области выполнения.
}
#endregion
}
}
Особенности реализации тестов¶
При реализации тестов, выполняющихся в общей области выполнения, необходимо учитывать следующие особенности:
-
Параллельно выполняющиеся тесты не должны создавать, изменять, удалять или блокировать общие ресурсы.
Подобные действия без надлежащей синхронизации приведут к возникновению трудно воспроизводимого взаимного влияния между тестами.
Любые изменения общих ресурсов должны выполняться один раз при инициализации области выполнения.
Если без подобного изменения обойтись невозможно, то такие тесты должны выполняться строго последовательно.
-
Не следует использовать восстановление состояния общего ресурса в состояние перед выполнением теста для предотвращения взаимного влияния тестов, если этот тест выполняется параллельно.
Например, при параллельном выполнении тестов удаление карточки шаблона этапов в одном тесте не гарантирует выполнения других тестов подсистемы маршрутов без учёта удалённого шаблона этапов. В данном случае необходимо использовать фильтрацию шаблонов этапов по типу карточки/документа или другому уникальному для теста признаку.
Удаление объектов может быть полезно для нивелирования роста времени выполнения вызванного большим числом объектов в системе.
-
В названиях объектов используйте имя теста.
Имя теста можно получить следующим способом:
TestContext.CurrentContext.Test.Name
илиTestContext.CurrentContext.Test.FullName
. Для получения имени теста ограниченного по длине рекомендуется использовать методTestHelper.GetTestNameLimited
.Это позволит упростить определение в каком тесте был создан тот или иной объект. Если необходимо, добавьте постоянный суффикс. Наличие постоянного суффикса позволит упростить отладку из-за возможности поиска по нему.
Рекомендации по поиску ошибок¶
При выполнении тестов в одной области выполнения возможно возникновение взаимного влияния между ними. Взаимное влияние проявляется как изменение поведения теста при совместном выполнении с другими тестами. В тоже время при изолированном выполнении тест выполняется без ошибок.
Для определения причины ошибки необходимо:
- Проверить реализацию непройденного теста на соответствие рекомендациям.
-
Найти пару тестов оказывающих влияние друг на друга.
-
Определить набор из тестов которые могут оказывать влияние на непройденный тест.
Обычно, такие тесты имеют общие ресурсы или параметры (в том числе имеющие фильтрацию по условию) и т.п.
-
Отключить параллельный запуск тестов.
Это необходимо для сокращения возможных кандидатов тестов влияющих друг на друга. В некоторых случаях это может привести к тому, что ошибка перестанет воспроизводиться. Скорее всего это означает, что она может проявляться только при параллельном выполнении.
-
Выполнить поиск пары тестов.
Для упрощения поиска можно воспользоваться следующей стратегией. Разделите группу тестов, предположительно оказывающую влияние, пополам. Запустите на выполнение тесты-кандидаты и непройденный тест. Если ошибка не воспроизвелась, выберите другую половину тестов-кандидатов и повторите запуск. Если при выполнении было получено воспроизведение ошибки, то разделите группу тестов-кандидатов пополам из этого запуска и повторите выполнение тестов. Т.о. можно быстро сократить число тестов-кандидатов и найти оказывающий влияние тест.
-
-
Проанализировать полученную пару тестов для определения и устранения причины ошибки.
Работа с ресурсами, используемыми в тестах¶
Ресурсы, используемые в тестах, встраиваются в сборку для повышения производительности и упрощения их переноса.
Ресурсы должны располагаться в папке Resources
, расположенной в корневой папке проекта теста.
Например: Source\Tests\Tessa.Test.Client\Resources
или Source\Tests\Tessa.Test.Server\Resources
.
Расположение ресурсов, относительно директории Resources
Название директории |
Описание |
---|---|
Cards | Карточки |
Cards\DocumentTypes | Типы документов |
Cards\KrProcess | Настройки маршрутов |
Cards\Notifications | Уведомления |
Cards\NotificationTypes | Типы уведомлений |
Cards\Roles | Карточки ролей |
Cards\Roles\PostgreSql | Карточки ролей, специфичные для установки на СУБД PostgreSQL |
Cards\Roles\SqlServer | Карточки ролей, специфичные для установки на СУБД SQL Server |
Cards\Settings | Карточки настроек |
Cards\Workflow | Карточки, используемые в тестах WorkflowEngine |
Cards\Configuration | Карточки, импортированные из конфигурации решения. Для предотвращения конфликтов с карточками решения, не рекомендуется добавлять карточки, используемые только в тестах, в эту директорию . Их следует размещать в директории, расположенной в Cards . Если карточка размещена в одной из стандартных директорий, для загрузки можно использовать один из соответствующих методов, содержащихся в классе TestConfigurationBuilderExtensions или воспользоваться обобщённым методом TestConfigurationBuilder.ImportCardsFromDirectory |
Localization | Файлы локализации |
Sql | SQL-скрипты |
Tsd | Схемы данных. Схема представлена в виде единых файлов |
Types | Типы карточек |
Types\Cards | Типы карточек |
Types\Dialogs | Типы карточек диалогов |
Types\Files | Типы карточек файлов |
Types\Tasks | Типы карточек заданий |
Views | Представления |
Workplaces | Рабочие места |
Для добавления нового ресурса необходимо выполнить следующие действия:
-
Добавить ресурс или ссылку на него в папку
Resources
. -
Настроить встраивание ресурса в сборку в виде внедрённого ресурса.
Для упрощения использования и возможности настройки в решении используется элемент EmbeddedResourceEx
. Он расширяет функциональность элемента EmbeddedResource
в части используемого преобразования имени файла в имя встроенного в сборку ресурса. Для подключения элемента в файл проекта необходимо подключить файл Tessa.EmbeddedResourceEx.targets
с помощью элемента Import
:
<Import Project="$(ProjectDir)../../Tessa.EmbeddedResourceEx.targets" />
Warning
Импорт файла Tessa.EmbeddedResourceEx.targets
должен выполняться после всех использований элемента EmbeddedResourceEx
в файле проекта. Элементы EmbeddedResourceEx
, расположенные после импорта файла Tessa.EmbeddedResourceEx.targets
, обработаны не будут.
Tip
По умолчанию файл Tessa.EmbeddedResourceEx.targets
подключён в типовых проектах тестов.
Для элемента EmbeddedResourceEx
можно задавать: параметры элемента EmbeddedResource
, стандартные параметры и общие метаданные.
Пример использования EmbeddedResourceEx
для включения файлов, имеющих расширение *.jlocalization
, расположенных по пути ..\..\..\Configuration\Localization
относительно текущего каталога и отображаемых в редакторе в директории Resources\Localization
:
<EmbeddedResourceEx Include="..\..\..\Configuration\Localization\*.jlocalization" LinkBase="Resources\Localization" />
По умолчанию все объекты, указанные в папке Resources
, добавляются как встроенные. За это отвечает строка в файле проекта:
<EmbeddedResourceEx Include="Resources\**\*" />
Для исключения файлов filler.txt
, предназначенных для заполнения пустой папки, применяются следующие команды:
<EmbeddedResourceEx Remove="Resources\**\filler.txt" />
<None Remove="Resources\**\filler.txt" />
Методы, выполняющие загрузку данных из встроенных ресурсов, принимают обязательный параметр “Сборка, содержащая загружаемые ресурсы” типа System.Reflection.Assembly
. Для упрощения задания данного параметра рекомендуется класс теста сделать наследником класса Tessa.Test.Default.Shared.ResourceAssemblyManager
, предоставляющего свойство ResourceAssembly
, который возвращает сборку, содержащую встроенные ресурсы.
Warning
Имя включаемого файла не должно содержать тэга языка <file_name>[.language_tag]<.file_extension>
, например, <file_name>**.ms**<.file_extension>
. При его наличии ресурс будет включён в вспомогательную сборку и станет недоступен при использовании методов для работы с ресурсами, указанными ниже. Более подробно см. в Packaging and Deploying Resources in .NET Apps. Resource naming conventions.
Tip
Вспомогательные методы для работы с ресурсами расположены в классах: Tessa.Platform.AssemblyHelper
, Tessa.Test.Default.Shared.Cards.TestCardHelper
и Tessa.Test.Default.Shared.Kr.KrTestHelper
.
Note
По умолчанию методы, принимающие путь к встроенному ресурсу, получают путь с разделителями уровней папок в пути, если не указано иное.
Методы, работающие с путями к встроенному ресурсу принимают путь к встроенному ресурсу. Автоматическое преобразование не выполняется, если не указано иное. Для преобразования пути в путь к встроенному ресурсу предназначены методы Tessa.Platform.AssemblyHelper.GetResourcePath
.
Путь к встроенному ресурсу – это путь, состоящий из компонентов:
-
Простое имя сборки.
-
Имя ресурса.
Warning
При компиляции имя ресурса может преобразовываться. Дополнительную информация см. в:
Описание преобразования, применяемого по умолчанию при использовании элемента
EmbeddedResource
- Шаг 1. Из полного имени ресурса выделить информацию о содержащем его каталоге.
- Шаг 2. Разделить путь к включаемому ресурсу на элементы в соответствии с разделителями уровней папок:
\
,/
. - Шаг 3. Для каждого элемента пути выполнить преобразование:
- Шаг 3.1. Разделить элемент пути по символу
.
. - Шаг 3.2. Для каждого элемента выполнить преобразование:
- Шаг 3.2.1. Если первый символ текущего элемента удовлетворяет условию: символ относится к категории букв Unicode ИЛИ к знаку препинания, являющегося соединителем двух символов (обозначение в Unicode – “Pc”), – тогда добавить его к результирующей строке и перейти к шагу 3.2.3, иначе добавить к результирующей строке символ
_
и перейти к следующему шагу. - Шаг 3.2.2. Добавить к результирующей строке первый символ текущего элемента, если он относится к следующей категории Unicode:
- буквы или десятичные цифры;
- знаки препинания, являющиеся соединителями двух символов (обозначение в Unicode – “Pc”);
- непробельные символы, указывающие на изменения базового символа (обозначение в Unicode – “Mn”);
- символ ненулевой ширины, указывающий на изменения базового символа и влияющий на ширину его глифа (обозначение в Unicode – “Mc”);
- вложенный символ – непробельный несамостоятельный знак, который окружает все предыдущие символы до базового символа включительно (обозначение в Unicode – “Me”).
- Шаг 3.2.3. Если текущий элемент имеет длину, равную одному символу, тогда добавить символ
_
и перейти к шагу 3.2.5, иначе перейти к следующему шагу. - Шаг 3.2.4. Цикл по оставшимся символам текущего элемента.
- Добавить к результирующей строке текущий символ, если он относится к следующей категории Unicode, иначе добавить символ
_
:- буквы или десятичные цифры;
- знаки препинания, являющиеся соединителям двух символов (обозначение в Unicode – “Pc”);
- непробельный символ, указывающий на изменения базового символа (обозначение в Unicode – “Mn”);
- символ ненулевой ширины, указывающий на изменения базового символа и влияющий на ширину его глифа (обозначение в Unicode – “Mc”);
- вложенный символ – непробельный несамостоятельный знак, который окружает все предыдущие символы до базового символа включительно (обозначение в Unicode – “Me”).
- Добавить к результирующей строке текущий символ, если он относится к следующей категории Unicode, иначе добавить символ
- Шаг 3.2.5. Если есть необработанные элементы, тогда к результирующей строке добавить символ
.
и перейти к шагу 3.2.1, иначе перейти к следующему шагу.
- Шаг 3.2.1. Если первый символ текущего элемента удовлетворяет условию: символ относится к категории букв Unicode ИЛИ к знаку препинания, являющегося соединителем двух символов (обозначение в Unicode – “Pc”), – тогда добавить его к результирующей строке и перейти к шагу 3.2.3, иначе добавить к результирующей строке символ
- Шаг 3.3. Если есть необработанные элементы, тогда к результирующей строке добавить символ
.
и перейти к шагу 3.1, иначе вернуть результат объединения результирующей строки, содержащей путь к ресурсу, с символом.
и именем файла ресурса.
- Шаг 3.1. Разделить элемент пути по символу
Например, путь
Resources\Cards\1Test folder (1)\Test card.card
преобразуется вResources.Cards._1Test_folder__1_.Test card.card
.Пример полного пути для сборки Tessa.Test.Examples.Server:
-
Пространство имён по умолчанию (совпадает с простым именем сборки): Tessa.Test.Examples.Server.
-
Путь, по которому ресурс включён в сборку: Resources\Cards\Tests\RoutingSamples\Access rules\Права на мероприятие.jcard
Путь к встроенному ресурсу: Tessa.Test.Examples.Server.Resources.Cards.Tests.RoutingSamples.Access_rules.Права на мероприятие.jcard
При использовании элемента
EmbeddedResourceEx
применяемое преобразование описано в файлеTessa.EmbeddedResourceEx.targets
.
Использование локализации в тестах¶
Сервис локализации автоматически инициализируется при инициализации тестов, если класс, содержащий тесты, является наследником класса Tessa.Test.Default.Shared.TestBase
.
Локализация загружается из текущей сборки с тестами. Информацию о расположении ресурсов см. в п. Работа с ресурсами, используемыми в тестах.
Если класс с тестами не является наследником класса TestBase
, то локализацию можно инициализировать вручную с помощью метода Tessa.Test.Default.Shared.TestHelper.InitializeDefaultLocalizationAsync()
.
Метод устанавливает язык локализации по умолчанию: английский.
Отложенные действия¶
Для реализации простого и удобного механизма выполнения настройки объектов тестов применяются отложенные действия. Отложенное действие описывается классом Tessa.Test.Default.Shared.Kr.PendingAction
(приведены основные члены класса):
-
Свойства:
-
Name
– возвращает название отложенного действия; -
PreparationActions
– возвращает список действий, выполняющихся перед выполнением отложенного действия; -
AfterActions
– возвращает список действий, выполняющихся после выполнения отложенного действия; -
Info
– возвращает дополнительную информацию.
-
-
Методы:
-
ExecuteAsync(CancellationToken)
– выполняет отложенное действие и подготовительные действия.Все сообщения, образующие результат выполнения действия, предваряются информационными сообщениями, содержащими информацию о действии, ставшем его источником. Для удаления информационных сообщений необходимо к результату выполнения применить метод
TestValidationKeys.ExceptPendingActionValidationResult(IReadOnlyList<IValidationResultItem>)
. -
SetInfo(Dictionary<string, object>, bool)
– устанавливает значение свойстваInfo
; -
AddPreparationAction(IPendingAction)
– добавляет указанное действие в список действий, выполняющихся перед выполнением отложенного действия; -
AddAfterAction(IPendingAction)
– добавляет указанное действие в список действий, выполняющихся после выполнения отложенного действия.
-
Выполнение отложенных действий¶
Для выполнения отложенных действий используется метод GoAsync(Action<ValidationResult>, CancellationToken)
, описываемый интерфейсом Tessa.Test.Default.Shared.Kr.IPendingActionsExecutor<T>
.
Метод осуществляет проверку результатов выполнения. По умолчанию осуществляется проверка результатов выполнения на наличие ошибок с помощью метода заданного в контексте KrTestContext.ValidationFunc
, если указанное свойство возвращает значение null
, то для проверки используется метод ValidationAssert.IsSuccessful(ValidationResult)
, проверяющий результат валидации на наличие ошибок, если они присутствуют, то создаётся исключение NUnit.Framework.AssertionException
.
Для проверки результатов выполнения действий необходимо задать метод, выполняющий их проверку и, в случае её отрицательного результата, создание исключения типа NUnit.Framework.AssertionException
или NUnit.Framework.InconclusiveException
(более подробно см. в документации NUnit Assertions и Assumptions).
Tip
Методы, выполняющие проверку результатов валидации расположены в классах: Tessa.Test.Default.Shared.ValidationAssert
и Tessa.Test.Default.Shared.ValidationAssume
.
Реализация проверок результатов выполнения отложенных действий, отличных от проверки на наличие ошибок¶
Все методы, выполняющие проверку результатов выполнения, отличного от проверки на наличие ошибок (ValidationAssert.IsSuccessful
), принимают обязательный параметр типа Tessa.Test.Default.Shared.ValidationResultItemValidator
– объект, выполняющий проверку результата выполнения. Он имеет несколько конструкторов, позволяющих выполнить его инициализацию:
-
Сигнатура конструктора объекта
ValidationResultItemValidator
, позволяющего задать ожидаемое сообщение валидации.public ValidationResultItemValidator( IValidationResultItem item, int expectedCount = DefaultExpectedCount, string name = default)
-
validationFunc
– сообщение о валидации на равенство которому выполняется проверка. -
expectedCount
– ожидаемое число срабатываний объекта валидации. Значение по умолчанию: 1. -
name
– имя объекта, выполняющего валидацию. Рекомендуется указывать для облегчения отладки.Note
Рекомендуется использовать эту перегрузку, т.к. она позволяет наиболее просто и полно проверить корректность сообщения валидации.
-
-
Сигнатура конструктора объекта
ValidationResultItemValidator
, позволяющего задать метод, выполняющий валидацию ожидаемого сообщения валидации.public ValidationResultItemValidator( Func<IValidationResultItem, bool> validationFunc, int expectedCount = DefaultExpectedCount, string name = default);
-
validationFunc
– метод, выполняющий валидацию. -
expectedCount
– ожидаемое число срабатываний объекта валидации. Значение по умолчанию: 1. -
name
– имя объекта, выполняющего валидацию. Рекомендуется указывать для облегчения отладки.
-
-
Сигнатура конструктора объекта
ValidationResultItemValidator
, позволяющего задать тип и ключ, которые должны присутствовать в ожидаемом сообщении валидации.public ValidationResultItemValidator( IValidationResultItem item, int expectedCount = DefaultExpectedCount, string name = default)
-
type
– тип сообщения о валидации, которому должно соответствовать проверяемое сообщение. -
key
– ключ сообщения о результате валидации, которое должно иметь проверяемое сообщение. -
expectedCount
– ожидаемое число срабатываний объекта валидации. Значение по умолчанию: 1. -
name
– имя объекта, выполняющего валидацию. Рекомендуется указывать для облегчения отладки.Warning
Не рекомендуется использовать данный конструктор при задании значения
ValidationKey.Unknown
параметруkey
из-за невозможности гарантирования правильности выполнения проверки.Данный конструктор имеет смысл использовать при задании значения параметра
key
равнымValidationKey.Unknown
только при проверке на отсутствие сообщений валидации с таким ключом. Для этого необходимо задать параметрexpectedCount
равным 0.
-
Warning
Обратите внимание: валидация осуществляется для каждого выполняемого отложенного действия в отдельности, а не для результата выполнения всех запланированных действий.
Пример реализации проверок результатов валидации.
/// <summary>
/// Проверяет обработку ошибки выполнения запроса, заданного в SQL условии включения шаблона этапов в маршрут.
/// </summary>
[Test]
public async Task RecalcSqlConditionError()
{
const string brokenQuery = "brokenSql";
await new KrStageTemplateFactory(
await KrStageGroupDescriptor.GetDefaultStageGroupAsync(this.DbScope),
this.TestDocTypeID,
this.TestDocTypeName,
this.CardLifecycleDependencies)
.Create("Test template")
.SetSqlCondition(brokenQuery)
.AddStage("Test stage", StageTypeDescriptors.ApprovalDescriptor)
.Save()
.GoAsync(); /* (1) */
var clc = await this.CreateCardLifecycleCompanion()
.Create()
.WithDocType(this.TestDocTypeID, this.TestDocTypeName)
.ModifyDocument()
.Save()
.GoAsync(); /* (2) */
await clc
.Recalc() /* (3) */
.GoAsync((validationResult) => /* (4) */
{
ValidationAssert.HasErrors( /* (5) */
validationResult,
new[]
{
new ValidationResultItemValidator(
new ValidationResultItem(
ValidationKey.Unknown,
ValidationResultType.Error,
KrErrorHelper.SqlDesignTimeError(this.GetKrExecutionUnitStub(KrScriptType.Condition, name, defaultGroup.Name), "$KrProcess_ErrorMessage_SqlPerformersError", EmptyHolder<string>.Array),
default,
default,
nameof(KrStageExecutor),
Environment.NewLine + brokenQuery)), /* (6) */
new ValidationResultItemValidator((item) =>
item.Type == ValidationResultType.Error
&& item.Key == ValidationKeys.Exception
&& item.ObjectType == nameof(KrStageExecutor)
&& item.Message.Contains(brokenQuery, StringComparison.Ordinal)) /* (7) */
});
});
}
-
Создание тестового маршрута.
-
Создание тестовой карточки.
-
Выполнение действия – осуществление пересчёта маршрута.
-
Выполнение запланированных действий.
-
Проверка на наличие ожидаемых ошибок в результате валидации после выполнения действия.
Note
Всегда проверяйте, является ли ошибка ожидаемой или нет. Это позволяет более полно убедиться в работоспособности тестируемой функциональности.
-
Проверка на наличие сообщения валидации, соответствующего заданному объекту валидации.
-
Проверка на наличие сообщения валидации, соответствующего заданному методу валидации.
Tip
Иногда проверяемое сообщение может быть сложным или зависеть от неконтролируемых факторов. В таком случае следует попытаться выделить уникальную часть сообщения и проверить её наличие. Что позволит в большей степени гарантировать правильность проверки.
Выноски:
-
Создание тестового маршрута.
-
Создание тестовой карточки.
-
Выполнение действия – осуществление пересчёта маршрута.
-
Выполнение запланированных действий.
-
Проверка на наличие ожидаемых ошибок в результате валидации после выполнения действия.
Note
Всегда проверяйте, является ли ошибка ожидаемой или нет. Это позволяет более полно убедиться в работоспособности тестируемой функциональности.
-
Проверка на наличие сообщения валидации, соответствующего заданному объекту валидации.
-
Проверка на наличие сообщения валидации, соответствующего заданному методу валидации.
Tip
Иногда проверяемое сообщение может быть сложным или зависеть от неконтролируемых факторов. В таком случае следует попытаться выделить уникальную часть сообщения и проверить её наличие. Что позволит в большей степени гарантировать правильность проверки.
Warning
Не используйте конструкцию следующего вида:
ValidationResultBuilder validationResults = new ValidationResultBuilder(); CardLifecycleCompanion clc = ...;
clc.GoAsync(result => validationResults.Add(result));
// Проверка результата выполнения, например: ValidationAssert.IsSuccessful(validationResults);
Её использование потенциально опасно и может привести к трудно воспроизводимым ошибкам:
- Можно забыть проверить результат выполнения.
-
Записать в переменную
validationResults
результат выполнения нескольких действий и после этого выполнить проверку. Это приведёт к тому, что при возникновении ошибки в результатах выполнения будет получена, скорее всего, не оригинальная, а наведённая. Как пример, при получении карточки происходит ошибка, но мы её записываем, а не проверяем сразу, после чего далее в тесте обращаемся к секции карточки и получаем другую ошибку. В результатах выполнения будет именно другая ошибка, а не оригинальная. Это значительно усложняет поиск ошибки, особенно если она плавающая.Пример, демонстрирующий создание наведённой ошибки вместо оригинальной
using System.Threading.Tasks; using NUnit.Framework; using Tessa.Extensions.Default.Shared.Workflow.KrProcess; using Tessa.Platform.Data; using Tessa.Platform.Validation; using Tessa.Test.Default.Shared; using Tessa.Test.Default.Shared.Kr;
namespace Tessa.Test.Server { /// <summary> /// Пример, демонстрирующий некорректную валидацию результатов выполнения. /// </summary> [SetupTempDb( Dbms.SqlServer, TestHelper.TempConfigurationStringMs, TestHelper.DbScriptDefaultMs)] public sealed class BugTest : KrServerTestBase { /// <summary> /// Метод, демонстрирующий пример некорректной валидации результатов выполнения. /// </summary> [Test] public async Task BugTestMethod() { var results = new ValidationResultBuilder();
var clc = await this.CreateCardLifecycleCompanion() .Create() .GoAsync(result => results.Add(result));
await clc .ModifyDocument() .Save() .GoAsync(result => results.Add(result));
var sections = clc.Card.Sections[KrConstants.DocumentCommonInfo.Name];
ValidationAssert.IsSuccessful(results); } } }
Результат выполнения
BugTestMethod Источник: BugTest.cs строка 25 Длительность: 15,4 с
Сообщение: System.NullReferenceException : Object reference not set to an instance of an object.
Трассировка стека: BugTest.BugTestMethod() строка 39 GenericAdapter`1.BlockUntilCompleted() NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaiter) AsyncToSyncAdapter.Await(Func`1 invoke) TestMethodCommand.RunTestMethod(TestExecutionContext context) TestMethodCommand.Execute(TestExecutionContext context) <>c__DisplayClass1_0.<Execute>b__0() BeforeAndAfterTestCommand.RunTestMethodInThreadAbortSafeZone(TestExecutionContext context, Action action)
В приведённом примере видно, что в результатах выполнения указана ошибка, не являющаяся причиной некорректного выполнения теста. Исходной является ошибка:
Error: KrPermissionsManager: Card type uses document types, but current card has no specified document type.
Управление отложенными действиями¶
Объект, позволяющий работать с отложенными действиями, описывается интерфейсом Tessa.Test.Default.Shared.Kr.IPendingActionsProvider<TAction, T>
, реализуемым классом Tessa.Test.Default.Shared.Kr.PendingActionsProvider<TAction, T>
. Он предоставляет (приведены основные члены класса):
-
Свойства:
-
HasPendingActions
– возвращает значение, показывающее наличие запланированных отложенных действий; -
IsSealed
– возвращает значение, показывающее, является ли объект запечатанным.
-
-
Методы:
-
AddPendingAction(IPendingAction)
– добавляет указанное отложенное действие в список запланированных действий; -
GetLastPendingAction
– возвращает последнее добавленное отложенное действие; -
PreparePendingActions(List<IPendingAction>)
– подготавливает запланированные действия к выполнению; -
GoAsync(Action<ValidationResult>, CancellationToken)
– выполняет запланированные к выполнению отложенные действия и проверяет результат выполнения с помощью методаValidationAssert.IsSuccessful
.Warning
В реализации по умолчанию, объект автоматически запечатывается после выполнения метода
PreparePendingActions(List<IPendingAction>)
перед выполнением отложенных действий для предотвращения изменения списка отложенных действий другими отложенными действиями.Попытка добавления нового действия при выполнении приведёт к исключению
Tessa.Platform.ObjectSealedException
.После обработки списка отложенных действий запрет на добавление новых действий снимается.
-
Пример создания отложенного действия
/// <summary>
/// Устанавливает значение указанного поля строковой секции карточки.
/// </summary>
/// <typeparam name="T">Тип объекта, управляющего жизненным циклом карточки.</typeparam>
/// <param name="clc">Объект, содержащий карточку.</param>
/// <param name="section">Имя секции.</param>
/// <param name="field">Имя поля.</param>
/// <param name="newValue">Задаваемое значение.</param>
/// <returns>Объект <typeparamref name="T"/> для создания цепочки.</returns>
/// <remarks>
/// Этот метод реализуется с помощью отложенного выполнения. Для выполнения запрошенного действия необходимо вызвать метод <see cref="IPendingActionsExecutor.GoAsync(Action{ValidationResult}, CancellationToken)"/>.
/// </remarks>
public static T SetValue<T>(
this T clc,
string section,
string field,
object newValue)
where T : ICardLifecycleCompanion, IPendingActionsProvider<IPendingAction, T>
{
clc.AddPendingAction(
new PendingAction(
$"{nameof(CardLifecycleCompanionExtensions)}.{nameof(SetValue)}: Section name: \"{section}\", Field name: \"{field}\".",
(action, ct) =>
{
clc.GetCardOrThrow().Sections[section].Fields[field] = newValue;
return new ValueTask<ValidationResult>(ValidationResult.Empty);
}));
return clc;
}
Обычно не требуется создавать новые действия явным образом. Для упрощения кода можно использовать методы Tessa.Test.Default.Shared.Kr.PendingActionsProviderExtensions.ApplyAction(...)
.
Пример использования метода ApplyAction(T, Action<T, IPendingAction>, string)
public static T SetValue2<T>(
this T clc,
string section,
string field,
object newValue)
where T : ICardLifecycleCompanion, IPendingActionsProvider<IPendingAction, T>
{
clc.ApplyAction(
(action, ct) => clc.GetCardOrThrow().Sections[section].Fields[field] = newValue,
name: $"{nameof(CardLifecycleCompanionExtensions)}.{nameof(SetValue2)}: Section name: \"{section}\", Field name: \"{field}\".");
return clc;
}
Warning
Обратите внимание: лямбда-выражение, реализующее отложенное действие, не содержит методов, планирующих отложенные действия. Если они будут указаны, то при их выполнении возникнет исключение Tessa.Platform.ObjectSealedException
.
Управление жизненным циклом карточки¶
Стандартные действий с карточками описаны в интерфейсе Tessa.Test.Default.Shared.Kr.ICardLifecycleCompanion<T>
.
Интерфейс описывает методы для:
-
Создания карточки –
Create(Action<CardNewRequest> modifyRequestAction)
-
Загрузки карточки –
Load(Action<CardGetRequest> modifyRequestAction)
-
Сохранения карточки –
Save(Action<CardStoreRequest> modifyRequestAction)
-
Удаления карточки –
Delete(Action<CardDeleteRequest> modifyRequestAction)
и др.
Базовую реализацию описываемых методов предоставляет класс Tessa.Test.Default.Shared.Kr.CardLifecycleCompanion<T>
.
Методы не выполняют запрошенное действие немедленно, а только его планируют. Для выполнения запланированных действий следует вызвать метод Tessa.Test.Default.Shared.Kr.CardLifecycleCompanion<T>.GoAsync(Action{ValidationResult}, CancellationToken)
.
Некоторые методы предоставляют возможность задания дополнительной информации (Info
), передаваемой в действие, о чём есть соответствующий комментарий. Например, это все действия для работы с карточкой: Create
, Load
, Save
и Delete
.
Пример использования CardLifecycleCompanion
protected ICardLifecycleDependencies CardLifecycleDependencies { get; }
...
var currencyClc =
await new CardLifecycleCompanion(
DefaultCardTypes.CurrencyTypeID,
DefaultCardTypes.CurrencyTypeName,
this.CardLifecycleDependencies) /* (1) */
.Create() /* (2) */
.SetValue("Currencies", "Name", "test") /* (3) */
.Save() /* (4) */
.GoAsync(); /* (5) */
-
Инициализация объекта
CardLifecycleCompanion
, выполняющего управление карточкой типа “Валюта”. -
Планирование создания карточки.
-
Планирование задания полю
Name
секцииCurrencies
значенияtest
. -
Планирование сохранения карточки.
-
Выполнение запланированных действий и проверка результата выполнения с помощью метода
ValidationAssert.IsSuccessful
.
Выноски:
-
Инициализация объекта
CardLifecycleCompanion
, выполняющего управление карточкой типа “Валюта”. -
Планирование создания карточки.
-
Планирование задания полю
Name
секцииCurrencies
значенияtest
. -
Планирование сохранения карточки.
-
Выполнение запланированных действий и проверка результата выполнения с помощью метода
ValidationAssert.IsSuccessful
.
Объекты, предоставляющие возможность управления жизненным циклом карточки
Название |
Описание |
---|---|
CardLifecycleCompanion<T> | Предоставляет базовую реализацию ICardLifecycleCompanion<T> |
KrRouteProcessInstanceLifecycleCompanion | Предоставляет методы для управления процессом маршрута документа, запущенного в карточке, которой управляет этот объект |
WeProcessInstanceLifecycleCompanion | Предоставляет методы для управления жизненным циклом карточки, в которой запущен экземпляр бизнес-процесса |
KrSecondaryProcessBuilder | Предоставляет методы для создания и модификации карточки вторичного процесса |
KrStageGroupBuilder | Предоставляет методы для создания и модификации карточки группы этапов |
KrStageTemplateBuilder | Предоставляет методы для создания и модификации карточки шаблонов этапов |
ServerConfigurator | Предоставляет методы, выполняющие настройку параметров сервера |
KrSettingsConfigurator | Предоставляет методы, выполняющие настройку параметров типового решения (Правая панель -> Настройки -> Типовое решение) |
LicenseConfigurator | Предоставляет методы, выполняющие настройку лицензий (Правая панель -> Настройки -> Лицензия) |
Tip
Большинство методов, предназначенных для взаимодействия с объектом, управляющим карточкой, расположены в классе Tessa.Test.Default.Shared.Kr.CardLifecycleCompanionExtensions
.
Для централизованного изменения запросов, выполняемых объектами, реализующими интерфейс Tessa.Test.Default.Shared.Kr.ICardLifecycleCompanion<T>
, применяется объект, реализующий Tessa.Test.Default.Shared.Kr.ICardLifecycleCompanionRequestExtender
. Для изменения текущего запроса необходимо использовать параметр modifyRequestAction
соответствующего метода.
Генерация имён временных ресурсов, используемых в тестах¶
Общими временными ресурсами в настоящее время являются: база данных, файловое хранилище, кэш клиентской метаинформации.
Для создания имён ресурсов рекомендуется использовать методы, определяемые интерфейсом ITestNameResolver
:
GetFixtureNameAsync(Type)
– возвращает имя ресурса, полученное для указанного типа класса, содержащего тесты.GetFixtureDateTimeAsync
– возвращает значение параметра FixtureDate или текущую дату и время, если параметр не задан в конфигурационном файле.
Объект, его реализующий, можно получить из Unity-контейнера.
Для получения постоянных значений для текущего класса, содержащего выполняемый тест, следует использовать: TestBase.GetFixtureNameAsync
и TestBase.GetFixtureDateTimeAsync
.
Сборка мусора¶
Механизм сборки мусора предназначен для отслеживания времени жизни и удаления внешних ресурсов (временная база данных, файловое хранилище и т.п.) после завершения или повторного запуска тестов.
Механизм включает в себя постоянную базу данных и обработчики для удаления ресурсов разных типов.
Warning
-
Служебная база данных предназначена только для хранения информации не оказывающей влияние на выполнение тестов. Настоятельно не рекомендуется хранить в ней информацию, которая будет использоваться непосредственно в тестах. Это может привести к возникновения сложно отлаживаемых взаимных влияний между тестами.
-
Для полноценной работы необходимо настроить строку подключения
gc
.
По умолчанию отслеживание используется для:
- Баз данных, созданных с помощью атрибута
SetupTempDbAttribute
и его наследников. - Временного файлового хранилища, путь к которому возвращается методом
TestBase.GetFileStoragePathAsync
.
Для регистрации объектов и их сборки используется объект описываемый интерфейсом IExternalObjectManager
. Экземпляр объекта можно получить из Unity-контейнера – TestBase.UnityContainer
. Он предоставляет следующие члены (показаны не все объекты):
CollectAsync
– методы для сборки зарегистрированного с помощью методаRegisterForFinalize
мусора.RegisterForFinalize
– регистрирует указанный объект для отслеживания и последующего освобождения при сборки мусора.KeepAlive
– прекращает отслеживание указанного объекта.
Пример использования IExternalObjectManager
var externalObjectManager = this.UnityContainer.Resolve<IExternalObjectManager>();
// Создание объекта с информацией о внешнем ресурсе.
var obj = DbExternalObjectHandler.CreateObjectInfo(
connectionString,
dataProvider,
this.GetHashCode());
externalObjectManager.RegisterForFinalize(obj);
// Сборка всех объектов, имеющих указанного владельца.
// Должен совпадать с указанным при регистрации объекта.
await externalObjectManager.CollectAsync(this.GetHashCode());
// Сборка всех объектов, созданных 10 минут назад.
await externalObjectManager.CollectAsync(new TimeSpan(0, 10, 0));
Обработчики объектов¶
Для обработки каждого типа объектов используется свой обработчик.
Стандартные обработчики объектов
Тип ресурса |
Название |
---|---|
Redis (ExternalObjectTypes.Redis ) |
RedisExternalObjectHandler |
База данных (ExternalObjectTypes.Database ) |
DbExternalObjectHandler |
Папка (ExternalObjectTypes.Folder ) |
FolderExternalObjectHandler |
Файл (ExternalObjectTypes.File ) |
FileExternalObjectHandler |
Создание нового обработчика¶
Любой обработчик должен реализовывать интерфейс IExternalObjectHandler
. Для упрощения создания объекта с информацией о внешнем ресурсе рекомендуется создавать метод CreateObjectInfo
.
Создание обработчика
using System;
using System.IO;
using System.Threading.Tasks;
using Tessa.Platform;
using Tessa.Platform.Storage;
using Tessa.Platform.Validation;
namespace Tessa.Test.Default.Shared.GC.Handlers
{
/// <summary>
/// Обработчик внешнего ресурса типа "папка".
/// </summary>
public sealed class FolderExternalObjectHandler :
ExternalObjectHandlerBase
{
#region Constants And Static Fields
/// <summary>
/// Ключ, по которому в <see cref="ExternalObjectInfo.Info"/> содержится полный путь к папке.
/// Тип значения: <see cref="string"/>.
/// </summary>
public const string PathKey = "Path";
private static readonly Guid ObjectTypeID = ExternalObjectTypes.Folder;
#endregion
#region Constructors
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="FolderExternalObjectHandler"/>.
/// </summary>
public FolderExternalObjectHandler()
: base(ObjectTypeID)
{
}
#endregion
#region Base Overrides
/// <inheritdoc/>
public override ValueTask HandleAsync(IExternalObjectHandlerContext context)
{
var objInfo = context.ObjectInfo.Info;
var path = objInfo.Get<string>(PathKey);
if (!Path.IsPathFullyQualified(path))
{
context.ValidationResult.AddError(
this,
$"The parameter \"{PathKey}\" must contain the fully qualified path. Path: \"{path}\".");
return ValueTask.CompletedTask;
}
try
{
if (Directory.Exists(path))
{
Directory.Delete(path, recursive: true);
}
}
catch (DirectoryNotFoundException)
{
// ignored
}
return ValueTask.CompletedTask;
}
#endregion
#region Public Static Methods
/// <summary>
/// Создаёт объект обрабатываемого типа.
/// </summary>
/// <param name="path">Полный путь к объекту файловой системы.</param>
/// <param name="fixtureID">Идентификатор владельца объекта.
/// Обычно это значение, возвращаемое методом <see cref="object.GetHashCode()"/>,
/// где <see cref="object"/> - класс, содержащий текущий набор тестов,
/// в котором был создан внешний ресурс (test fixture).</param>
/// <returns>Созданный объект.</returns>
public static ExternalObjectInfo CreateObjectInfo(
string path,
int fixtureID)
{
Check.ArgumentNotNullOrWhiteSpace(path, nameof(path));
var obj = new ExternalObjectInfo()
{
ID = Guid.NewGuid(),
TypeID = ObjectTypeID,
Created = DateTime.UtcNow,
FixtureID = fixtureID,
};
obj.Info[PathKey] = path;
return obj;
}
#endregion
}
}
Регистрация обработчика в Unity-контейнере
using System.Threading.Tasks;
using Tessa.Test.Default.Shared;
using Tessa.Test.Default.Shared.GC.Handlers;
using Unity;
using Unity.Lifetime;
namespace Tessa.Test.Server
{
public class ExampleTest :
TestBase
{
/// <inheritdoc/>
protected override async ValueTask InitializeContainerAsync(
IUnityContainer container)
{
await base.InitializeContainerAsync(container);
container
.RegisterType<IExternalObjectHandler, FolderExternalObjectHandler>(
nameof(FolderExternalObjectHandler),
new ContainerControlledLifetimeManager());
}
}
}
Создание атрибута, выполняющего действия до и/или после теста¶
Для создания пользовательских атрибутов, предоставляющих такую функциональность, в NUnit предназначен базовый абстрактный атрибут https://docs.nunit.org/articles/nunit/extending-nunit/Action-Attributes.html[NUnit.Framework.TestActionAttribute]
.
Пример создания атрибута, выполняющего действия до и после теста
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using Tessa.Test.Default.Shared;
namespace Tessa.Test.Server
{
/// <summary>
/// Пример атрибута, выполняющего действия до и после теста.
/// </summary>
/// <remarks>
/// Для примера, в стандартный вывод записывается строка, содержащая полное имя выполнявшегося метода, а в свойство <see cref="IDataContainer.Value"/> текущего TestFixtute (класса, содержащего тест), реализующего интерфейс <see cref="IDataContainer"/> устанавливается полное имя текущего теста.
/// </remarks>
public class ExampleActionAttribute :
TestActionAttribute
{
#region Base Overrides
/// <inheritdoc/>
public override ActionTargets Targets => ActionTargets.Test;
/// <inheritdoc/>
public override void BeforeTest(ITest test)
{
base.BeforeTest(test);
// Обратите внимание на использование списков действий, которые позволяют:
// 1. Единообразно обрабатывать действия. Так действие, выполняемое в данном методе (ITestAction.BeforeTest(ITest)), выполняется до метода, отмеченного атрибутом NUnit.Framework.SetUpAttribute;
// 2. Гибко управлять порядком выполнения планируемых действий;
// 3. Добавлять новое действие в определённое место. Место вставки можно найти, например, следующим способом:
// var testActions = test.Get<ITestActions>();
// var index = testActions.GetTestActions(ActionStage.BeforeSetUp).IndexOf(i => i.Sender.GetType() == typeof(Tessa.Test.Default.Shared.SetupDbScopeAttribute));
// 4. Не требуется использовать конструкцию task.GetAwaiter().GetResult() при работе с асинхронным кодом.
var testActions = test.Get<ITestActions>();
testActions.GetTestActions(ActionStage.BeforeSetUp).Add(new TestAction(this, BeforeSetUpActionAsync));
// Информация о действии, выполняющемся после теста, должна добавляться в методе ITestAction.BeforeTest(ITest), а не в ITestAction.AfterTest(ITest), т.к. он выполняется после выполнения методов, отмеченных атрибутом NUnit.Framework.TearDownAttribute.
testActions.GetTestActions(ActionStage.AfterTearDown).Add(new TestAction(this, AfterTearDownActionAsync));
}
#endregion
#region Private Methods
private static ValueTask BeforeSetUpActionAsync(object sender)
{
var currentTest = TestHelper.TestExecutionContext.CurrentTest;
var dataContainer = currentTest.Get<IDataContainer>();
dataContainer.Value = currentTest.FullName;
var instance = (ExampleActionAttribute)sender;
Console.WriteLine(instance.GetType().FullName + "." + nameof(BeforeSetUpActionAsync));
return new ValueTask();
}
private static ValueTask AfterTearDownActionAsync(object sender)
{
var currentTest = TestHelper.TestExecutionContext.CurrentTest;
var dataContainer = currentTest.Get<IDataContainer>();
dataContainer.Value = default;
var instance = (ExampleActionAttribute)sender;
Console.WriteLine(instance.GetType().FullName + "." + nameof(AfterTearDownActionAsync));
return new ValueTask();
}
#endregion
}
}
using System;
using NUnit.Framework;
using Tessa.Test.Default.Shared;
namespace Tessa.Test.Server
{
public interface IDataContainer
{
/// <summary>
/// Возвращает или задаёт значение, задаваемое в атрибуте <see cref="ExampleActionAttribute"/>
/// </summary>
string Value { get; set; }
}
[ExampleAction]
public sealed class ExampleActionAttributeTest :
TestBase,
IDataContainer
{
#region IDataContainer Members
/// <inheritdoc/>
public string Value { get; set; }
#endregion
#region Tests
[Test]
public void TestMethod()
{
Console.WriteLine("Current value: " + this.Value);
}
#endregion
}
}
Результат выполнения теста доступен на странице обозревателя тестов по ссылке “Открыть дополнительные выходные данные для этого результата”:
Выполнение операций в тестах¶
Для выполнения операций, созданных с помощью IOperationRepository
, в тестах предназначен объект ITestOperationExecutor
. Объект можно получить из серверного Unity-контейнера.
ITestOperationExecutor
позволяет безопасно выполнять операции с учётом параллельного выполнения тестов.
Пример выполнения операций, создаваемых при обработке асинхронных связей Workflow Engine
Полную реализацию см. в WeProcessInstanceLifecycleCompanion.ProcessAsyncOperations
.
private IDbScope DbScope { get; }
private ITestOperationExecutor TestOperationExecutor { get; }
private IWorkflowEngineProcessor WorkflowEngineProcessor { get; }
/// <summary>
/// Выполняет асинхронные операции, созданные экземпляром бизнес-процесса с заданным идентификатором.
/// </summary>
/// <param name="executeNewOperations">Значение <see langword="true"/>, если необходимо выполнить операции созданные после выполнения других операций, иначе - <see langword="false"/>, если необходимо выполнить только текущие операции.</param>
/// <param name="processID">Идентификатор экземпляра бизнес-процесса.</param>
/// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param>
/// <returns>Результат выполнения.</returns>
private async Task<ValidationResult> ProcessAsyncOperationsAsync(
bool executeNewOperations,
Guid processID,
CancellationToken cancellationToken = default)
{
var validationResult = new ValidationResultBuilder();
List<Guid>? processIDs = null;
await this.TestOperationExecutor.ExecuteOperationsAsync(
async context =>
{
// Проверка возможности выполнения операции.
var operation = context.Operation;
if (operation.State == OperationState.InProgress)
{
return false;
}
var requestStorage = operation
.Request
?.Info
.TryGet<Dictionary<string, object?>>("Request");
if (requestStorage is null)
{
context.ValidationResult.AddError(
this,
$"No {nameof(WorkflowEngineProcessRequest)} found in operation request info for operation {operation.ID:B}");
return false;
}
var request = new WorkflowEngineProcessRequest();
request.Deserialize(requestStorage);
var currentProcessInstanceID = request.ProcessInstanceID;
if (!currentProcessInstanceID.HasValue)
{
context.ValidationResult.AddError(
this,
$"Process ID is not specified in operation with ID = {operation.ID:B}.");
return false;
}
// Текущая операция относится к процессу, не входящему в группу процессов, управляемых экземпляром процесса с идентификатором processID?
if (processIDs?.Contains(currentProcessInstanceID.Value) == false)
{
return false;
}
// Выполнение операции.
await context.OperationRepository.StartAsync(
operation.ID,
operation.TypeID,
context.CancellationToken);
var result = await this.WorkflowEngineProcessor.ProcessSignalAsync(
request,
cancellationToken: context.CancellationToken);
context.ValidationResult.Add(result.ValidationResult);
return true;
},
new TestOperationExecutorOptions(
OperationTypes.WorkflowEngineAsync,
validationResult)
{
ExecuteNewOperations = executeNewOperations,
DeleteOperation = true,
},
async context =>
{
// Подготовка к выполнению операций.
// Метод выполняется один раз перед обработкой первой операции из предварительно сформированного списка операций.
// Здесь формируется список идентификаторов экземпляров процессов, которыми управляет экземпляр процесса с идентификатором processID.
// Список обновляется каждый раз после завершения выполнения предварительно отобранных по типу операции. Это позволяет учитывать возможный запуск подпроцесса Workflow Engine.
processIDs = await this.GetTreeProcessesAsync(
processID,
context.CancellationToken);
},
cancellationToken);
return validationResult.Build();
}
/// <summary>
/// Возвращает список идентификаторов экземпляров процессов, которыми управляет экземпляр процесса с заданным идентификатором.
/// </summary>
/// <param name="processID">Идентификатор родительского экземпляра процесса.</param>
/// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param>
/// <returns>Список идентификаторов экземпляров процессов, которыми управляет экземпляр процесса с заданным идентификатором.</returns>
private async Task<List<Guid>> GetTreeProcessesAsync(
Guid processID,
CancellationToken cancellationToken = default)
{
await using var _ = this.DbScope.Create();
var db = this.DbScope.Db;
return await db
.SetCommand(
this.DbScope.BuilderFactory
.With("ChildProcess", e => e
.Select()
.P("RootRowID")
.UnionAll()
.Select()
.C("p", "RowID")
.From("WorkflowEngineProcesses", "p").NoLock()
.InnerJoin("ChildProcess", "cp")
.On().C("cp", "RowID").Equals().C("p", "ParentRowID"),
columnNames: new[] { "RowID" },
recursive: true)
.Select()
.C("p", "RowID")
.From("WorkflowEngineProcesses", "p").NoLock()
.InnerJoin("ChildProcess", "cp")
.On().C("cp", "RowID").Equals().C("p", "RowID")
.Build(),
db.Parameter("RootRowID", processID))
.LogCommand()
.ExecuteListAsync<Guid>(cancellationToken);
}