Перейти к основному содержанию
`Ctrl`+`K`

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

## Unit-тестирование

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

Для создания набора тестов создайте класс, который будет наследоваться от LangFunSuite, ApiTest или других классов, в которые подмешаны трейты из ScalaTest.

Совет

Для дополнительной информации смотрите библиотеку unit тестирования: scalatest.

### Создание класса с тестами

1. Перейдите в окно проекта.

2. Выберете целевой модуль.

3. Перейдите в папку с исходными кодами:
   `[module_name]/src/test/scala`

   Совет

   Создать недостающую папку можно из контекстного меню в idea `New > Directory`.

4. Создайте пакет `ru.bitec.app.[module_name]`.

5. Создайте тестовый класс:

        class Lesson1Test extends LangFunSuite{
          test("HelloWorld"){
              println("hello world")
          }
        }

Совет

Запустите тест из контекстного меню, для этого:

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

2. В контекстном меню выполните операцию „Debug“ для запуска в режиме отладки или „Run“ для простого запуска.

Подробнее смотрите выполнение тестов в руководстве idea.

Примечание

Существуют два специализированных базовых класса для тестовых случаев:

- `LangFunSuite` - используется для тестов, которые не нуждаются в контроллерах бизнес логики.

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

- `ApiTest` - в данном тексте доступен контекст автономной бизнес логики. Тестовые случаи могут использовать `Api`,`Pkg` объекты. При этом запуск теста становится медленней из за необходимости инициализировать контекст.

### Базовые Assert методы

Трейт Assertions содержит основные методы для проверки предположений.

1. Assert(condition: Boolean).

Этот метод проверяет условие. Если переданное условие возвращает true, то метод завершается нормально, иначе выбрасывает ошибку TestFailedException.

Пример:

      test("создание объекта класса Btk_SomeClass") {
        try {
          val rop = Btk_SomeClassApi().insert()
          assert(Btk_SomeClassExtApi().findSomeClassExt(rop).isDefined)
        } finally {
          session.rollback()
        }
      }

Данный тест проверяет, что при создании объекта Btk_SomeClass для него создаётся расширение Btk_SomeClassExt. Если расширение не было создано, то тест не будет пройден.
В консоль будет выведен текст ошибки :

    Btk_SomeClassExtApi.apply().findSomeClassExt(rop).isDefined was false

В assert можно передать дополнительный комментарий, который будет добавляться к тексту ошибки:

      test("создание объекта класса Btk_SomeClass") {
        try {
          val rop = Btk_SomeClassApi().insert()
          assert(rop.get(_.dBeginDate).isNotNull, "При создании не установилась дата начала.")
          assert(Btk_SomeClassExtApi().findSomeClassExt(rop).isDefined, "При создании не зарегистрировалось расширение.")
        } finally {
          session.rollback()
        }
      }

2. assertResult(expected: Any)(actual: Any).

Сравнивает ожидаемое ожидаемое значение с переданным:

      test("parseGtkSessionClientFullName") {
        val x = WorkSessionClientHelper.parseGtkSessionClientFullName("DESkTOP-23565:8080#E1@123-45")
        assert(x.isDefined)
        assertResult("DESkTOP-23565:8080")(x.get._1)
        assertResult("E1")(x.get._2)
        assertResult("123-45")(x.get._3)
      }

3. assertThrows[T <: AnyRef](f: => Any).

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

      test("data.oaObjQty.isEmpty") {
        val data = new Bs_DistrData()
        assertThrows[AppException] {
          Bs_DistributionPkg().distribQty(data)
        }
      }

4. assume(condition: Boolean).

Метод aналогичен методу assert, но в случае, когда не выполняется условие, не завершается ошибкой, а отменяет тест. Так же может содержать пояснительный комментарий:

    assume(database.isAvailable, "База данных не доступна.")
    assume(database.getAllUsers.count === 9)

5. intercept[T <: AnyRef](f: => Any).

Метод aналогичен методу assertThrows, но позволяет получить ошибку ожидаемого типа для более детальной проверки:

            val vEx = intercept[AppException] {
              _api.validateOnBeforeManualInsert(ropDuplicate)
            }
            assert(vEx.getMessage.startsWith("Ошибка. Уже существует запись с классом типа объектов:"))

### Matchers

Трейт Matchers позволяет использовать в тестах комбинаторы вроде shouldBe вместо обычного assert, улучшая читаемость кода.

По умолчанию к ApiTest и LangFunSuite Matchers не подключен, поэтому чтобы использовать его методы, добавьте его в объявлении класса:

    class Bs_AccApiTest extends ApiTest with Matchers {
      test("set level on insert") {
        val rop = api.insert()
        rop.get(_.nLevel) should ===(1.nn)
      }
    }

Для дополнительной информации смотрите в руководстве пользователя: matchers.

Некоторые примеры:

1. Проверка размера и длины.

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

- <проверяемый объект> should have length (<ожидаемое значение>);

- <проверяемый объект> should have size (<ожидаемое значение>).

Пример:

      test("проверка размера массива") {
        val result = getArgs()
        result should have length (3)
      }

2. Проверка строк:

- на наличие подстроки:

        string should startWith ("Hello")
        string should endWith ("world")
        string should include ("seven")

- с использованием регулярных выражений:

        string should startWith regex ("Hel*o")
        string should endWith regex ("wo.ld")
        string should include regex ("wo.ld")

- проверка всей строки на соответствие регулярному выражению:

        string should fullyMatch regex ("""(-)?(\d+)(\.\d*)?""")

3. Сравнения больше/меньше/равно:
   Можно сравнивать объекты любого типа, кроме тех, которые могут быть преобразованы в Ordered[T]. Для сравнения списков, используйте greater than, less than, greater than or equal, less than or equal to a value of type T

        one should be < (7)
        one should be > (0)
        one should be <= (7)
        one should be >= (0)

- с использованием регулярных выражений:

        string should startWith regex ("Hel*o")
        string should endWith regex ("wo.ld")
        string should include regex ("wo.ld")

- проверка всей строки на соответствие регулярному выражению:

        string should fullyMatch regex ("""(-)?(\d+)(\.\d*)?""")

## Массовое тестирование модулей

### Из консоли sbt

- Для запуска тестов по имени/модулю воспользуйтесь командой `testOnly`:

    testOnly {путь до класса}.{имя класса}

Например:

    testOnly ru.bitec.app.pro.diagram.Pro_DiagramApiTest

Символ `*` заменяет любое количество символов в наименовании теста, таким образом можно запустить выполнение тестов не по полному имени, а по части наименования. Например, запустить тесты по модулю можно так:

    testOnly ru.bitec.app.{ваш модуль}.*

Например:

     testOnly ru.bitec.app.tax.*

- Для запуска тестов всего проекта воспользуйтесь командой `test`:

     test

Примечание

Не рекомендуется использовать, запускайте этот сценарий только на тех проектах, где точно известно, что все тестовые классы не делают нежелательных изменений в файлах или в БД.

- Для запуска только тех тестов, которые связаны с последними изменениями в коде, используйте команду testQuick, которую можно вызывать с такой же фильтрацией, как и testOnly.

### Запуск сценариев тестирования с использованием JMeter

- Для запуска скачайте и установите JMeter.

- Скачайте пример сценария, запускающего тестирование, и откройте его в `JMeter`:

  `пример проекта`

- Данный проект содержит три примера сценария:

  - TestByModule - тестирование по модулю;

  - TestByClass - тестирование по классу;

  - TestAll - запуск всех тестов проекта.

- Выберите нужный тред и сделайте его активным (операция контекстного меню Enable. Disable - сделает выбранный элемент неактивным).

- Развернув выбранный тред, отредактируйте параметры компонента run test:

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

  - В параметрах замените исполняемую команду (Примеры см. выше), исправьте модуль или имя файла.

- Запустите тестирование через операцию Start на тулбаре. Выполнение тестирования может занять продолжительное время. Дождитесь окончания.

- Результаты выполнения будут отображены в View Results Tree. Для более детального отчета можно посмотреть логи на вкладке «Response data».

  ### Выборочное исключение тестов из массового тестирования

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

## Jexl-тесты

В меню `Настройки и сервисы` - `Сервисы Jexl` - `Jexl-тесты` можно писать и выполнять jexl-тесты.

### Создание

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

На закладке «Глобальные переменные» можно задать переменные, к которым можно обращаться в тесте через методы `tst.setGlobalVar` и `tst.getGlobalVar`. Для их создания необходимо указать системное имя переменной, которая должна быть уникальна для теста, и ее значение в виде jexl-скрипта, который будет выполняться перед стартом теста.

#### Доступные методы

##### tst.setVar

Записывает в контекст выполнения jexl-теста переданное значение. Информация об изменении переменной записывается в лог выполнения теста с типом `SETVAR`.

Пример:

    tst.setVar("param_name", param_value); //param_name - имя параметра, param_value - значение параметра

##### tst.getVar

Достает из контекста значение параметра по его имени.

Пример:

    var p = tst.getVar("param_name"); //param_name - имя параметра

##### tst.setGlobalVar

Записывает в глобальные переменные выполнения jexl-теста переданное значение. Доступ к этим переменным есть на любом шаге теста, без необходимости получать нужный контекст. Информация об изменении переменной записывается в лог выполнения теста с типом `SETVAR`.

Пример:

    tst.setGlobalVar("param_name", param_value); //param_name - имя параметра, param_value - значение параметра

##### tst.getGlobalVar

Получает значение глобальной переменной по ее имени.

Пример:

    var p = tst.getGlobalVar("param_name"); //param_name - имя параметра

##### tst.findParentContext

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

Пример:

    var parent = tst.findParentContext();
    var parentParam = parent.getVar("param_name");

##### tst.info

Записывает в лог выполнения теста переданный текст с типом `INFO`.

Пример:

    tst.info("Текст с информацией");

##### tst.warning

Записывает в лог выполнения теста переданный текст с типом `WARNING`.

Пример:

    tst.warning("Текст с предупреждением");

##### tst.error

Записывает в лог выполнения теста переданный текст с типом `ERROR`.

Пример:

    tst.error("Текст с ошибкой");

##### tst.raise

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

Пример:

    tst.raise("Текст с ошибкой");

##### tst.assertTrue

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

Пример:

    tst.assertTrue(1 == 2); //запишет в лог "assertTrue не прошло проверку"

    tst.assertTrue(1 == 2, "Число 1 не равно числу 2"); //запишет в лог переданный текст "Число 1 не равно числу 2"

##### tst.shouldBeTrue

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

Пример:

    tst.shouldBeTrue(1 == 2); //запишет в лог "shouldBeTrue не прошло проверку"

    tst.shouldBeTrue(1 == 2, "Число 1 не равно числу 2"); //запишет в лог переданный текст "Число 1 не равно числу 2"

##### tst.sqlRows

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

Пример:

    var count = tst.sqlRows(`select t.id from RplTst_AllDbTypes t where t.idObjectType = 120`);

### Выполнение

Выполнение теста осуществляется в помощью операции `Выполнить тест` в карточке теста или списке тестов. Для выполнения тестов целой группы необходимо в списке тестов выполнить операцию `Выполнение всех тестов текущей группы`.

#### Выполнение с помощью Rest сервиса

Выполнение тестов также возможно с помощью Rest сервиса.

Путь для выполнения одиночного теста: `http://{имя_сервера}/app/sys/rest/ss/pkg/Bts_TestPkg/runSingleTest`

Путь для выполнения группы тестов: `http://{имя_сервера}/app/sys/rest/ss/pkg/Bts_TestPkg/runTestForGroup`

Тело обоих запросов является json-объектом, который состоит из ключа `name` и его значения, в виде системного имени теста или группы, соответственно.

Запрос выполнения одиночного теста возвращает json-объект, который содержит в себе следующие параметры: имя теста `name`, наличие ошибок `hasErrors`, наличие предупреждений `hasWarnings`, количество ошибок `countErrors` и количество предупреждений `countWarnings`.

Пример для одиночного теста:

    //пример обращения
    curl -H Database:pgDev -u admin:admin -d {\"name\":\"PRS_PurchReqTest\"} -H "Content-Type: application/json" -X POST http://localhost:8080/app/sys/rest/ss/pkg/Bts_TestPkg/runSingleTest

    //пример результата
    {"hasErrors":true,"hasWarnings":false,"countErrors":1,"countWarnings":0,"name":"Тест заявок на закупку услуг"}

Запрос выполнения группы тестов возвращает json-объект, который содержит в себе следующие параметры: наличие ошибок `hasErrors`, наличие предупреждений `hasWarnings`, количество ошибок `countErrors`, количество предупреждений `countWarnings` и результаты выполнения всех тестов `tests` в виде массива json-объектов таких же, как при выполнении одиночного теста.

    //пример обращения
    curl -H Database:pgDev -u admin:admin -d {\"name\":\"Test1\"} -H "Content-Type: application/json" -X POST http://localhost:8080/app/sys/rest/ss/pkg/Bts_TestPkg/runTestForGroup

    //пример результата
    {"hasErrors":true,"hasWarnings":false,"countErrors":2,"countWarnings":0,"tests":[{"hasErrors":false,"hasWarnings":false,"countErrors":0,"countWarnings":0,"name":"test_ok"},{"hasErrors":true,"hasWarnings":false,"countErrors":1,"countWarnings":0,"name":"test333"},{"hasErrors":true,"hasWarnings":false,"countErrors":1,"countWarnings":0,"name":"test22"}]}

# Статический анализ

## Использование

Статический анализ происходит во время компиляции кода.

1. Частичная сборка - покажет где в измененных файлах есть ошибки.

2. Полная пересборка - покажет где в исходниках есть ошибки.

## Добавление правил

Уже существующие правила.

Правила добавляются в build.sbt, или для всех модулей в sbt-plugin CommonSettings.

    val settings = Seq(
      wartremoverWarnings ++= Seq(
        Wart.custom("ru.bitec.warts.NetWart"),
        Wart.ListAppend
      )
    )

## Написание правил

1. Кастомные правила пишутся в этот проект.

2. Официальная документация wartremover.

3. Документация по обходу синтаксического дерева scala.
   Главный помощник для написания правил это функция reify.

        import scala.reflect.runtime.universe._
        val tree = reify {
          println(2)
        }.tree // это дерево не содержит информацию о типах

        /*
         Apply( вызов метода
              Select(
                  Select(This(TypeName("scala")), TermName("Predef")), на чем вызывают метод
                  TermName("println") что вызывают
                ),
              List(Literal(Constant(2))) параметры
            )
         */
        val raw = showRaw(tree)