Перейти к содержанию

Автоматические тесты NUnit

Системные требования

Разработка и выполнение тестов в IDE

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

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

Windows

  • Windows 7 SP1 / 8.1 / 10 / 11

  • .NET SDK 5.0.xxx последней доступной версии.

Linux

Сборки с тестами

Классы с тестами располагаются в специальных сборках с клиентскими или серверными тестами.

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”
    }

    Параметр содержит:

    1. Строки подключения к тестовой базе данных TESSA.

      • default – строка подключения по умолчанию. Используется, если подключение явно не задано.

      • temp_ms – строка подключения к временной базе данных SQL Server.

      • temp_pg – строка подключения к временной базе данных PostgreSQL.

      Информацию о формате строки подключения см. в Руководстве по установке СЭД TESSA.

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

      Если тесты выполняются на постоянной базе, то необходимо указать в строке подключения по умолчанию (default) параметры подключения к существующей базе данных, иначе можно указать любое допустимое имя, например, tessa-test, в этом случае база данных, с именем, начинающимся на указанную строку, будет автоматически создана.

    2. Служебные строки подключения.

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

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

Подготовительные действия

  • Серверные тесты

    1. При выполнении тестов на постоянной базе данных необходимо в строке подключения файла конфигурации серверных тестов Source\Tests\Tessa.Test.Server\app.json указать подключение к базе данных, которая будет использоваться при выполнении тестов.

    2. Обновить тестовую конфигурацию, выполнив пакетный файл Configuration.Test\Update.bat.

    3. После обновления тестовой конфигурации необходимо пересобрать проекты, содержащие тесты.

  • Клиентские тесты

    Клиентские тесты могут выполняться на постоянной или временной базе данных.

    • Выполнение тестов на постоянной базе данных.

      1. Выполнить установку нового экземпляра системы TESSA с указанием тестовой базы данных.

      2. Проверить правильность заданных параметров в файлах конфигурации.

      Note

      • Для проведения тестирования не требуется выполнять настройку Tessa Applications.

      • Сервис Chronos не требуется устанавливать в качестве системной службы. Его можно запустить один раз после завершения установки.

      • В строке подключения файла конфигурации клиентских тестов Source\Tests\Tessa.Test.Client\app.json необходимо указать подключение к базе данных, на которую была выполнена установка нового экземпляра системы.

    • Выполнение тестов на временной базе данных.

      1. Обновить тестовую конфигурацию, выполнив пакетный файл Configuration.Test\Update.bat.

      2. После обновления тестовой конфигурации необходимо пересобрать проекты, содержащие тесты.

Выполнение и отладка тестов в IDE

В данном разделе кратко рассматривается выполнение и отладка тестов в IDE на примере Microsoft Visual Studio 2019. При использовании другой IDE – обратитесь к её документации.

Для просмотра и выполнения тестов используется Обозреватель тестов. Открыть Обозреватель тестов можно через меню Visual Studio: Вид → Обозреватель тестов или Тест → Обозреватель тестов.

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

В контекстном меню, доступном на элементах дерева представляющего список тестов, доступны команды, позволяющие запустить тесты для выполнения и отладки, а также выполнить переход к исходному коду теста.

Для отладки теста необходимо:

  • Установить точки останова в исходном коде отлаживаемых тестов.

  • Запустить отлаживаемые тесты, выбрав в контекстном меню элемента Обозревателя тестов пункт Отладка.

    Также, можно запускать тесты в режиме отладки из меню кнопки Запустить, расположенной на панели инструментов Обозревателя тестов или Меню Visual Studio → Тест.

Отладка тестов в IDE на Linux

Предварительные требования
  1. На компьютере с Visual Studio необходимо установить рабочую нагрузку ASP.NET и разработка веб-приложений или Кроссплатформенная разработка .NET.

  2. Установить SSH сервер.

    Для установки выполните следующую команду в консоли:

    sudo apt-get install openssh-server unzip curl

Создание и развёртывание приложения

Подготовка приложения для отладки.

  1. Выберите конфигурацию проекта Отладка.

  2. Убедитесь, что проект настроен на создание переносимых PDB-файлов (параметр по умолчанию), что PDB-файлы находятся в том же расположении, что и библиотека DLL. Чтобы выполнить эту настройку в Visual Studio, щёлкните проект правой кнопкой мыши, затем выберите Свойства → Сборка → Дополнительно → Сведения об отладке.

    Note

    Проекты Tessa.Test.(Client | Server | Share) настроены на создание переносимых PDB-файлов.

Для развёртывания приложения перед отладкой можно использовать несколько методов.

  • Скопируйте источники на целевой компьютер и выполните сборку с помощью dotnet build или dotnet test на компьютере Linux.

  • Выполните сборку приложения в Windows, а затем перенесите артефакты сборки на компьютер Linux (артефакты сборки состоят из самого приложения, любых библиотек среды выполнения, от которых он может зависеть, и файла *.deps.json).

Note

Для упрощения изложения в примерах будет использоваться способ запуска тестов из сборки, собранной на Windows.

Подключение отладчика
  1. Включение режима ожидания подключения отладчика к хост-процессу, выполняющему тесты.

    В консоли необходимо выполнить команду, которая установит переменную окружения, включающую режим ожидания подключения отладчика к процессу dotnet:

    export VSTEST_HOST_DEBUG=1

  2. Запуск тестов.

    В этой же консоли необходимо выполнить команду, запускающую тесты на выполнение, например, следующей командой (подробное описание команд dotnet test см. в п. Выполнение тестов из консоли):

    dotnet test Tessa.Test.Server.dll

    вместо выполнения тестов dotnet test будет ожидать подключения отладчика. В сообщении будет указан идентификатор и имя процесса, к которому необходимо подключиться.

    На приведённом рисунке это:

    • имя процесса: dotnet;

    • идентификатор процесса: 102967.

  3. Присоединение к процессу.

    В 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
Модули
  • Визуализатор маршрутов
  • Диаграммы и графики
  • Кластеризация
  • Конструктор бизнес-процессов
  • Мобильное согласование
  • Несколько файловых хранилищ
  • Потоковый ввод документов
  • Редакция Enterprise
  • Синхронизация с Active Directory / LDAP
  • Форумы и обсуждения

Описание API тестов

Note

Для упрощения работы с тестами мы рекомендуем соблюдать следующие соглашения наименования классов, предоставляющих тесты:

  • Имя класса, содержащего тесты, должно заканчиваться на слово Test;

  • Имя класса, являющегося базовым по отношению к классам, содержащим тесты, должно заканчиваться на TestBase;

  • Имя класса, тесты в котором выполняются на определённой базе данных, должно заканчиваться на SqlServer или PostgreSql, если используется подключение к базе данных SQL Server или PostgreSQL соответственно.

Пример использования API тестов

Note

Для загрузки доступен проект, содержащий тесты для примеров процессов, реализованных с использованием подсистемы маршрутов и бизнес-процессов WorkflowEngine.

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

Установка примеров:

  1. Извлеките из архива папку Tessa.Test.Examples.Server в директорию <Папка сборки>\Source\Tests\.

  2. Скачайте архивы с примерами использования маршрутов и бизнес-процессов.

  3. В папке со сборкой создайте директорию rnd.

  4. Распакуйте содержимое архива с примерами маршрутов в директорию <Папка сборки>\rnd\RoutingSamples\.

  5. Извлеките папку Configuration из архива с примерами бизнес-процессов в директорию <Папка сборки>\rnd\WorkflowExamples\.

  6. Извлеките папку Extensions из архива с примерами бизнес-процессов в директорию <Папка сборки>\Source\.

  7. Откройте решение <Папка сборки>\Source\Tessa.Extensions.sln и добавьте в него проект <Папка сборки>\Source\Tests\Tessa.Test.Examples.Server\Tessa.Test.Examples.Server.csproj. Для этого щелкните правкой кнопкой мыши по узлу Tests в обозревателе решений и выберите в контекстном меню Добавить → Существующий проект…​, после чего выберите добавляемый проект.

  8. В решение <Папка сборки>\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
    }
}
  1. Используется подключение по умолчанию.

  2. Удаление тестовой карточки после завершения всех тестов.

  3. Удаление карточки правила доступа после завершения всех тестов.

Обратите внимание на отличия клиентского теста от серверного:

  1. Используется подключение по умолчанию.

  2. Удаление тестовой карточки после завершения всех тестов.

  3. Удаление карточки правила доступа после завершения всех тестов.

Пример простого клиентского теста, выполняемого на временной базе данных с использованием специально созданного сервера.

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
    }
}
  1. Используется подключение к временной базе данных, как в серверном тесте.

  2. Используется базовый класс KrHybridClientTestBase.

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

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

  2. Используется базовый класс 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
    }
}
  1. Используется подключение по умолчанию.

  2. Удаление тестовой карточки, созданной в тесте SaveCard, после завершения всех тестов.

  3. Удаление тестовой карточки, созданной в тесте RegisterDocument, после завершения всех тестов.

  4. Удаление карточки правила доступа после завершения всех тестов.

Обратите внимание на отличия клиентского теста от серверного:

  1. Используется подключение по умолчанию.

  2. Удаление тестовой карточки, созданной в тесте SaveCard, после завершения всех тестов.

  3. Удаление тестовой карточки, созданной в тесте RegisterDocument, после завершения всех тестов.

  4. Удаление карточки правила доступа после завершения всех тестов.

Управление сессиями в клиентских тестах

По умолчанию клиентские тесты выполняются от имени пользователя, указанного в параметре 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
    }
}
  1. Уровень доступа “Администратор” необходим для создания второго пользователя от имени первого.

Использование 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
    }
}
  1. Используется подключение к временной базе данных, как в серверном тесте.

  2. Используется базовый класс KrHybridClientTestBase.

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

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

  2. Используется базовый класс 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
    }
}
  1. В данном тесте после выполнения действий процесс не завершается для проверки контролируемого значения (номер цикла согласования), хранящегося в параметрах процесса.

  2. Проверка выполнения скрипта инициализации задания при создании задания.

  3. Проверка наличия указанного числа заданий типа “Доработка”.

  4. Проверка состояния карточки.

  5. Завершение задания и проверка действий, выполняющихся при этом: выполнение скрипта завершения задания и выполнение перехода на действие “Сценарий”.

    Если необходимо завершить задание или выполнить действие от имени другого пользователя, то его необходимо разместить в блоке 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(); }

  6. Получение экземпляра процесса.

  7. Проверка значения параметра процесса, содержащего номер цикла согласования.

Выноски:

  1. В данном тесте после выполнения действий процесс не завершается для проверки контролируемого значения (номер цикла согласования), хранящегося в параметрах процесса.

  2. Проверка выполнения скрипта инициализации задания при создании задания.

  3. Проверка наличия указанного числа заданий типа “Доработка”.

  4. Проверка состояния карточки.

  5. Завершение задания и проверка действий, выполняющихся при этом: выполнение скрипта завершения задания и выполнение перехода на действие “Сценарий”.

    Если необходимо завершить задание или выполнить действие от имени другого пользователя, то его необходимо разместить в блоке 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(); }

  6. Получение экземпляра процесса.

  7. Проверка значения параметра процесса, содержащего номер цикла согласования.

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) */
    ;
  1. Планирование импорта всех типов карточек.

  2. Планирование импорта карточек в соответствии с Tessa_(ms | pg).cardlib.

  3. Планирование настройки календаря.

  4. Получение объекта конфигуратора ServerConfigurator, выполняющего настройку сервера.

  5. Планирование создания или получения карточки настроек сервера. Если карточки нет в базе данных, то она будет создана.

    Если вызов конфигуратора выполняется в клиентских тестах и карточка отсутствует в базе данных, то следует использовать метод Create().

  6. Планирование инициализации карточки настроек сервера.

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

  8. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

  9. Получение объекта конфигуратора KrDocTypesConfigurator типов документов.

  10. Установка в качестве настраиваемой карточки типа документа карточки с идентификатором this.TestDocTypeID. Если карточка не загружена в кэш конфигуратора, то она будет создана. Для загрузки существующей карточки из базы данных необходимо задать значение true параметру isLoad. Если тип документа создаётся, то должен быть указан также и идентификатор типа карточки. В примере это – this.TestCardTypeID.

  11. Планирование разрешения использования согласования для настраиваемого типа документа.

  12. Планирование разрешения использования регистрации для настраиваемого типа документа.

  13. Планирование разрешения использования типового процесса отправки задач для настраиваемого типа документа.

  14. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

  15. Получение объекта конфигуратора KrSettingsConfigurator, выполняющего настройку типового решения.

  16. Инициализация конфигуратора – получение карточки типового решения. Если карточки нет в базе данных, то она будет создана.

  17. Получение объекта конфигуратора KrSettingsTypeConfigurator, выполняющего настройку типа карточки, имеющего идентификатор, возвращаемый свойством this.TestCardTypeID.

  18. Планирование разрешения использования типов документов для карточки настраиваемого типа.

  19. Возврат к конфигуратору верхнего уровня – KrSettingsConfigurator.

  20. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

  21. Получение объекта конфигуратора LicenseConfigurator, выполняющего настройку лицензий.

  22. Инициализация конфигуратора – получение карточки лицензий. Если карточки нет в базе данных, то она будет создана.

  23. Планирование добавления указанного пользователя в список “Мобильное согласование” в карточке “Лицензия”.

  24. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

  25. Получение объекта конфигуратора PermissionsConfigurator для настройки правил доступа.

  26. Установка в качестве настраиваемой карточки, карточки правила доступа с заданным идентификатором. Если карточка с указанным идентификатором отсутствует в кэше конфигуратора, то она будет создана. Для загрузки существующей карточки из базы данных необходимо задать значение true параметру isLoad.

  27. Планирование задания всех разрешений.

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

    Если необходимо, например, добавить новую роль к существующему списку ролей, то можно объединить старый и новый списки следующим образом:

    .ModifyRoles(p => p.Concat(new[] { this.Session.User.ID }))

    Для использования метода Concat требуется подключить пространство имён System.Linq.

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

  30. Планирование изменения списка состояний карточки, в которых будут работать определённые разрешения.

  31. Изменение карточки правила доступа. Планирование удаления карточки правила доступа после завершения теста. В серверных тестах данное действие обычно не требуется выполнять, кроме случаев, когда тестирование выполняется на постоянной базе данных.

    Для управления удалением карточек после выполнения тестов предназначены объекты, предоставляемые свойствами TestCardManager и TestCardManagerOnce класса Tessa.Test.Default.Shared.TestBase.

  32. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

    Warning

    Обратите внимание: если не выполнить возврат к конфигуратору верхнего уровня, то при выполнении запланированных действий будут выполнены действия, запланированные в текущем конфигураторе. В данном случае – PermissionsConfigurator. Действия, запланированные в других конфигураторах, выполнены не будут.

  33. Выполнение запланированных действий и проверка результата выполнения.

Выноски:

  1. Планирование импорта всех типов карточек.

  2. Планирование импорта карточек в соответствии с Tessa_(ms | pg).cardlib.

  3. Планирование настройки календаря.

  4. Получение объекта конфигуратора ServerConfigurator, выполняющего настройку сервера.

  5. Планирование создания или получения карточки настроек сервера. Если карточки нет в базе данных, то она будет создана.

    Если вызов конфигуратора выполняется в клиентских тестах и карточка отсутствует в базе данных, то следует использовать метод Create().

  6. Планирование инициализации карточки настроек сервера.

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

  8. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

  9. Получение объекта конфигуратора KrDocTypesConfigurator типов документов.

  10. Установка в качестве настраиваемой карточки типа документа карточки с идентификатором this.TestDocTypeID. Если карточка не загружена в кэш конфигуратора, то она будет создана. Для загрузки существующей карточки из базы данных необходимо задать значение true параметру isLoad. Если тип документа создаётся, то должен быть указан также и идентификатор типа карточки. В примере это – this.TestCardTypeID.

  11. Планирование разрешения использования согласования для настраиваемого типа документа.

  12. Планирование разрешения использования регистрации для настраиваемого типа документа.

  13. Планирование разрешения использования типового процесса отправки задач для настраиваемого типа документа.

  14. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

  15. Получение объекта конфигуратора KrSettingsConfigurator, выполняющего настройку типового решения.

  16. Инициализация конфигуратора – получение карточки типового решения. Если карточки нет в базе данных, то она будет создана.

  17. Получение объекта конфигуратора KrSettingsTypeConfigurator, выполняющего настройку типа карточки, имеющего идентификатор, возвращаемый свойством this.TestCardTypeID.

  18. Планирование разрешения использования типов документов для карточки настраиваемого типа.

  19. Возврат к конфигуратору верхнего уровня – KrSettingsConfigurator.

  20. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

  21. Получение объекта конфигуратора LicenseConfigurator, выполняющего настройку лицензий.

  22. Инициализация конфигуратора – получение карточки лицензий. Если карточки нет в базе данных, то она будет создана.

  23. Планирование добавления указанного пользователя в список “Мобильное согласование” в карточке “Лицензия”.

  24. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

  25. Получение объекта конфигуратора PermissionsConfigurator для настройки правил доступа.

  26. Установка в качестве настраиваемой карточки, карточки правила доступа с заданным идентификатором. Если карточка с указанным идентификатором отсутствует в кэше конфигуратора, то она будет создана. Для загрузки существующей карточки из базы данных необходимо задать значение true параметру isLoad.

  27. Планирование задания всех разрешений.

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

    Если необходимо, например, добавить новую роль к существующему списку ролей, то можно объединить старый и новый списки следующим образом:

    .ModifyRoles(p => p.Concat(new[] { this.Session.User.ID }))

    Для использования метода Concat требуется подключить пространство имён System.Linq.

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

  30. Планирование изменения списка состояний карточки, в которых будут работать определённые разрешения.

  31. Изменение карточки правила доступа. Планирование удаления карточки правила доступа после завершения теста. В серверных тестах данное действие обычно не требуется выполнять, кроме случаев, когда тестирование выполняется на постоянной базе данных.

    Для управления удалением карточек после выполнения тестов предназначены объекты, предоставляемые свойствами TestCardManager и TestCardManagerOnce класса Tessa.Test.Default.Shared.TestBase.

  32. Возврат к конфигуратору верхнего уровня – TestConfigurationBuilder.

    Warning

    Обратите внимание: если не выполнить возврат к конфигуратору верхнего уровня, то при выполнении запланированных действий будут выполнены действия, запланированные в текущем конфигураторе. В данном случае – PermissionsConfigurator. Действия, запланированные в других конфигураторах, выполнены не будут.

  33. Выполнение запланированных действий и проверка результата выполнения.

Настройка импорта карточек в тестовую базу данных

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

  • IsImportCards – возвращает значение, показывающее, необходимо ли выполнять импорт карточек из конфигурации.
  • IsImportFileTemplateCards – возвращает значение, показывающее, необходимо ли выполнять импорт карточек шаблонов файлов из конфигурации.
  • ImportCardsAsync – импортирует карточки в тестовую базу данных.
  • ImportCardPredicateAsync – фильтрует импортируемые карточки. Используется, если свойство IsImportCards имеет значение true.
  • ImportFileTemplateCardPredicateAsync – фильтрует импортируемые карточки шаблонов файлов. Используется, если свойство IsImportFileTemplateCards имеет значение true.

Рассмотрим типовую реализацию в классе KrServerTestBaseKrHybridClientTestBase аналогичная):

/// <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);

  1. Флаг, разрешающий импорт карточек.
  2. Флаг, разрешающий импорт файловых шаблонов.
  3. Создание карточки настроек сервера по умолчанию для использования в тестах. Карточка настроек сервера, создаваемая для использования в тестах, имеет отличие только в используемом хранилище по умолчанию: FileSystem (ID = 2), местоположение равно значению, возвращаемому методом TestHelper.GetFileStoragePath. Более подробно про настройку файлового хранилища в тестах читайте в следующем разделе.

  4. Планирование импорта карточек в соответствии с библиотекой карточек Tessa_(ms | pg).cardlib, если свойство IsImportCards имеет значение true. Для фильтрации импортируемых карточек используется метод ImportCardPredicateAsync.

  5. Планирование импорта карточек в соответствии с библиотекой карточек File templates.cardlib, если свойство IsImportFileTemplateCards имеет значение true. Для фильтрации импортируемых карточек используется метод ImportFileTemplateCardPredicateAsync.
  6. При импорте карточек исключаются карточки, соответствующие типу “Настройки сервера” для того, чтобы они не перезаписали карточку, созданную в п. 3.

Выноски:

  1. Флаг, разрешающий импорт карточек.
  2. Флаг, разрешающий импорт файловых шаблонов.
  3. Создание карточки настроек сервера по умолчанию для использования в тестах. Карточка настроек сервера, создаваемая для использования в тестах, имеет отличие только в используемом хранилище по умолчанию: FileSystem (ID = 2), местоположение равно значению, возвращаемому методом TestHelper.GetFileStoragePath. Более подробно про настройку файлового хранилища в тестах читайте в следующем разделе.

  4. Планирование импорта карточек в соответствии с библиотекой карточек Tessa_(ms | pg).cardlib, если свойство IsImportCards имеет значение true. Для фильтрации импортируемых карточек используется метод ImportCardPredicateAsync.

  5. Планирование импорта карточек в соответствии с библиотекой карточек File templates.cardlib, если свойство IsImportFileTemplateCards имеет значение true. Для фильтрации импортируемых карточек используется метод ImportFileTemplateCardPredicateAsync.
  6. При импорте карточек исключаются карточки, соответствующие типу “Настройки сервера” для того, чтобы они не перезаписали карточку, созданную в п. 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-&lt;Имя_области_выполнения&gt;. К примеру, если имя области выполнения – 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 } }

Особенности реализации тестов

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

  1. Параллельно выполняющиеся тесты не должны создавать, изменять, удалять или блокировать общие ресурсы.

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

    Любые изменения общих ресурсов должны выполняться один раз при инициализации области выполнения.

    Если без подобного изменения обойтись невозможно, то такие тесты должны выполняться строго последовательно.

  2. Не следует использовать восстановление состояния общего ресурса в состояние перед выполнением теста для предотвращения взаимного влияния тестов, если этот тест выполняется параллельно.

    Например, при параллельном выполнении тестов удаление карточки шаблона этапов в одном тесте не гарантирует выполнения других тестов подсистемы маршрутов без учёта удалённого шаблона этапов. В данном случае необходимо использовать фильтрацию шаблонов этапов по типу карточки/документа или другому уникальному для теста признаку.

    Удаление объектов может быть полезно для нивелирования роста времени выполнения вызванного большим числом объектов в системе.

  3. В названиях объектов используйте имя теста.

    Имя теста можно получить следующим способом: TestContext.CurrentContext.Test.Name или TestContext.CurrentContext.Test.FullName. Для получения имени теста ограниченного по длине рекомендуется использовать метод TestHelper.GetTestNameLimited.

    Это позволит упростить определение в каком тесте был создан тот или иной объект. Если необходимо, добавьте постоянный суффикс. Наличие постоянного суффикса позволит упростить отладку из-за возможности поиска по нему.

Рекомендации по поиску ошибок

При выполнении тестов в одной области выполнения возможно возникновение взаимного влияния между ними. Взаимное влияние проявляется как изменение поведения теста при совместном выполнении с другими тестами. В тоже время при изолированном выполнении тест выполняется без ошибок.

Для определения причины ошибки необходимо:

  1. Проверить реализацию непройденного теста на соответствие рекомендациям.
  2. Найти пару тестов оказывающих влияние друг на друга.

    1. Определить набор из тестов которые могут оказывать влияние на непройденный тест.

      Обычно, такие тесты имеют общие ресурсы или параметры (в том числе имеющие фильтрацию по условию) и т.п.

    2. Отключить параллельный запуск тестов.

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

    3. Выполнить поиск пары тестов.

      Для упрощения поиска можно воспользоваться следующей стратегией. Разделите группу тестов, предположительно оказывающую влияние, пополам. Запустите на выполнение тесты-кандидаты и непройденный тест. Если ошибка не воспроизвелась, выберите другую половину тестов-кандидатов и повторите запуск. Если при выполнении было получено воспроизведение ошибки, то разделите группу тестов-кандидатов пополам из этого запуска и повторите выполнение тестов. Т.о. можно быстро сократить число тестов-кандидатов и найти оказывающий влияние тест.

  3. Проанализировать полученную пару тестов для определения и устранения причины ошибки.

Работа с ресурсами, используемыми в тестах

Ресурсы, используемые в тестах, встраиваются в сборку для повышения производительности и упрощения их переноса.

Ресурсы должны располагаться в папке 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 Рабочие места

Для добавления нового ресурса необходимо выполнить следующие действия:

  1. Добавить ресурс или ссылку на него в папку Resources.

  2. Настроить встраивание ресурса в сборку в виде внедрённого ресурса.

Для упрощения использования и возможности настройки в решении используется элемент 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>, например, &lt;file_name&gt;**.ms**&lt;.file_extension&gt;. При его наличии ресурс будет включён в вспомогательную сборку и станет недоступен при использовании методов для работы с ресурсами, указанными ниже. Более подробно см. в 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.

Путь к встроенному ресурсу – это путь, состоящий из компонентов:

  1. Простое имя сборки.

  2. Имя ресурса.

    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”).
        • Шаг 3.2.5. Если есть необработанные элементы, тогда к результирующей строке добавить символ . и перейти к шагу 3.2.1, иначе перейти к следующему шагу.
      • Шаг 3.3. Если есть необработанные элементы, тогда к результирующей строке добавить символ . и перейти к шагу 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) */
                });
        });
}
  1. Создание тестового маршрута.

  2. Создание тестовой карточки.

  3. Выполнение действия – осуществление пересчёта маршрута.

  4. Выполнение запланированных действий.

  5. Проверка на наличие ожидаемых ошибок в результате валидации после выполнения действия.

    Note

    Всегда проверяйте, является ли ошибка ожидаемой или нет. Это позволяет более полно убедиться в работоспособности тестируемой функциональности.

  6. Проверка на наличие сообщения валидации, соответствующего заданному объекту валидации.

  7. Проверка на наличие сообщения валидации, соответствующего заданному методу валидации.

    Tip

    Иногда проверяемое сообщение может быть сложным или зависеть от неконтролируемых факторов. В таком случае следует попытаться выделить уникальную часть сообщения и проверить её наличие. Что позволит в большей степени гарантировать правильность проверки.

Выноски:

  1. Создание тестового маршрута.

  2. Создание тестовой карточки.

  3. Выполнение действия – осуществление пересчёта маршрута.

  4. Выполнение запланированных действий.

  5. Проверка на наличие ожидаемых ошибок в результате валидации после выполнения действия.

    Note

    Всегда проверяйте, является ли ошибка ожидаемой или нет. Это позволяет более полно убедиться в работоспособности тестируемой функциональности.

  6. Проверка на наличие сообщения валидации, соответствующего заданному объекту валидации.

  7. Проверка на наличие сообщения валидации, соответствующего заданному методу валидации.

    Tip

    Иногда проверяемое сообщение может быть сложным или зависеть от неконтролируемых факторов. В таком случае следует попытаться выделить уникальную часть сообщения и проверить её наличие. Что позволит в большей степени гарантировать правильность проверки.

    Warning

    Не используйте конструкцию следующего вида:

    ValidationResultBuilder validationResults = new ValidationResultBuilder(); CardLifecycleCompanion clc = ...;

    clc.GoAsync(result => validationResults.Add(result));

    // Проверка результата выполнения, например: ValidationAssert.IsSuccessful(validationResults);

    Её использование потенциально опасно и может привести к трудно воспроизводимым ошибкам:

    1. Можно забыть проверить результат выполнения.
    2. Записать в переменную 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>.

Интерфейс описывает методы для:

  1. Создания карточки – Create(Action<CardNewRequest> modifyRequestAction)

  2. Загрузки карточки – Load(Action<CardGetRequest> modifyRequestAction)

  3. Сохранения карточки – Save(Action<CardStoreRequest> modifyRequestAction)

  4. Удаления карточки – 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) */
  1. Инициализация объекта CardLifecycleCompanion, выполняющего управление карточкой типа “Валюта”.

  2. Планирование создания карточки.

  3. Планирование задания полю Name секции Currencies значения test.

  4. Планирование сохранения карточки.

  5. Выполнение запланированных действий и проверка результата выполнения с помощью метода ValidationAssert.IsSuccessful.

Выноски:

  1. Инициализация объекта CardLifecycleCompanion, выполняющего управление карточкой типа “Валюта”.

  2. Планирование создания карточки.

  3. Планирование задания полю Name секции Currencies значения test.

  4. Планирование сохранения карточки.

  5. Выполнение запланированных действий и проверка результата выполнения с помощью метода 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.

По умолчанию отслеживание используется для:

  1. Баз данных, созданных с помощью атрибута SetupTempDbAttribute и его наследников.
  2. Временного файлового хранилища, путь к которому возвращается методом 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); }

Back to top