# Тестирование ## Unit-тестирование При необходимости проверки работоспособности отдельных частей исходного кода имеется возможность реализации unit-тестирования на основе ScalaTest. Для создания набора тестов необходимо создать класс, который будет наследоваться от LangFunSuite, ApiTest или других классов, в которые подмешаны трейты из ScalaTest. ```{tip} Для дополнительной информации смотрите библиотеку unit тестирования: [scalatest](https://www.scalatest.org/) ``` ### Создание класса с тестами 1. Перейдите в окно проекта 2. Выберете целевой модуль 3. Перейдите в папку с исходными кодами \ `[module_name]/src/test/scala` ```{tip} Создать недостающую папку можно из контекстного меню в idea `New > Directory` ``` 4. Создайте пакет `ru.bitec.app.[module_name]` 5. Создайте тестовый класс ```scala class Lesson1Test extends LangFunSuite{ test("HelloWorld"){ println("hello world") } } ``` ```{tip} Запустить тест можно из контекстного меню, для этого: 1. Переведите курсор на декларацию функции или класса, если нужно запустить все объявленные тесты класса 2. В контекстном меню выполните операцию 'Debug' для запуска в режиме отладки или 'Run' для простого запуска Подробнее смотрите [выполнение тестов](https://www.jetbrains.com/help/idea/performing-tests.html) в руководстве idea. ``` ```{note} Существуют два специализированных базовых класса для тестовых случаев: - `LangFunSuite` \ Используется для тестов которые не нуждаются в контроллерах бизнес логики. Данные тесты не могут использовать `Api`, `Pkg`, и не имеют подключения к базе данных по умолчанию. - `ApiTest` \ В данном тексте доступен контекст автономной бизнес логики, тестовые случаи могут использовать `Api`,`Pkg` объекты. При этом запуск теста становится медленней из за необходимости инициализировать контекст. ``` ### Базовые Assert методы Трейт Assertions содержит основные методы для проверки предположений. 1. assert(condition: Boolean) Этот метод проверяет условие. Если переданное условие возвращает true, то метод завершается нормально, иначе выбрасывает ошибку TestFailedException. Пример: ```scala 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 можно передать дополнительный комментарий, который будет добавляться к тексту ошибки: ```scala 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) Сравнивает ожидаемое ожидаемое значение с переданным ```scala 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) Используется,когда нужно удостоверится, что при определенных методах тестируемый код выдаст ошибку. ```scala test("data.oaObjQty.isEmpty") { val data = new Bs_DistrData() assertThrows[AppException] { Bs_DistributionPkg().distribQty(data) } } ``` 4. assume(condition: Boolean) Метод aналогичен методу assert, но в случае, когда не выполняется условие, не завершается ошибкой, а отменяет тест. Так же может содержать пояснительный комментарий. ```scala assume(database.isAvailable, "База данных не доступна.") assume(database.getAllUsers.count === 9) ``` 5. intercept[T <: AnyRef](f: => Any) Метод aналогичен методу assertThrows, но позволяет получить ошибку ожидаемого типа для более детальной проверки. ```scala val vEx = intercept[AppException] { _api.validateOnBeforeManualInsert(ropDuplicate) } assert(vEx.getMessage.startsWith("Ошибка. Уже существует запись с классом типа объектов:")) ``` ### Matchers Трейт Matchers позволяет использовать в тестах комбинаторы вроде shouldBe вместо обычного assert, улучшая читаемость кода. По умолчанию к ApiTest и LangFunSuite Matchers не подключен, поэтому чтобы использовать его методы, добавьте его в объявлении класса: ```scala class Bs_AccApiTest extends ApiTest with Matchers { test("set level on insert") { val rop = api.insert() rop.get(_.nLevel) should ===(1.nn) } } ``` Для дополнительной информации смотрите в руководстве пользователя: [matchers](https://www.scalatest.org/user_guide/using_matchers) Некоторые примеры: 1. Проверка размера и длины Для проверки размера и длины используйте: - <проверяемый объект> should have length (<ожидаемое значение>) - <проверяемый объект> should have size (<ожидаемое значение>) Пример: ```scala test("проверка размера массива") { val result = getArgs() result should have length (3) } ``` 2. Проверка строк - на наличие подстроки ```scala string should startWith ("Hello") string should endWith ("world") string should include ("seven") ``` - с использованием регулярных выражений ```scala string should startWith regex ("Hel*o") string should endWith regex ("wo.ld") string should include regex ("wo.ld") ``` - проверка всей строки на соответствие регулярному выражению ```scala 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 ```scala one should be < (7) one should be > (0) one should be <= (7) one should be >= (0) ``` - с использованием регулярных выражений ```scala string should startWith regex ("Hel*o") string should endWith regex ("wo.ld") string should include regex ("wo.ld") ``` - проверка всей строки на соответствие регулярному выражению ```scala 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 ``` ```{note} Не рекомендуется использовать, запускайте этот сценарий только на тех проектах, где точно известно, что все тестовые классы не делают нежелательных изменений в файлах или в БД. ``` - Для запуска только тех тестов, которые связаны с последними изменениями в коде, используйте команду testQuick, которую можно вызывать с такой же фильтрацией, как и testOnly. ### Запуск сценариев тестирования с использованием JMeter - Для запуска скачайте и установите [JMeter](https://jmeter.apache.org/download_jmeter.cgi) - Скачайте пример, запускающего тестирование, и откройте его в `JMeter`: ```{only} html {download}`пример проекта <./files/unitTest.jmx>` ``` ```{only} latex Обратитесь к дистрибьютору документации за файлом `files/unitTest.jmx` ``` - Данный проект содержит три примера сценария: * 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` Пример: ```scala tst.setVar("param_name", param_value); //param_name - имя параметра, param_value - значение параметра ``` ##### tst.getVar Достает из контекста значение параметра по его имени. Пример: ```scala var p = tst.getVar("param_name"); //param_name - имя параметра ``` ##### tst.setGlobalVar Записывает в глобальные переменные выполнения jexl-теста переданное значение. Доступ к этим переменным есть на любом шаге теста, без необходимости получать нужный контекст. Информация об изменении переменной записывается в лог выполнения теста с типом `SETVAR` Пример: ```scala tst.setGlobalVar("param_name", param_value); //param_name - имя параметра, param_value - значение параметра ``` ##### tst.getGlobalVar Получает значение глобальной переменной по ее имени. Пример: ```scala var p = tst.getGlobalVar("param_name"); //param_name - имя параметра ``` ##### tst.findParentContext Возвращает контекст предыдущего шага выполнения теста. Из него можно получить параметры, записанные в контекст на предудущем шаге. Пример: ```scala var parent = tst.findParentContext(); var parentParam = parent.getVar("param_name"); ``` ##### tst.info Записывает в лог выполнения теста переданный текст с типом `INFO`. Пример: ```scala tst.info("Текст с информацией"); ``` ##### tst.warning Записывает в лог выполнения теста переданный текст с типом `WARNING`. Пример: ```scala tst.warning("Текст с предупреждением"); ``` ##### tst.error Записывает в лог выполнения теста переданный текст с типом `ERROR`. Пример: ```scala tst.error("Текст с ошибкой"); ``` ##### tst.raise Выбрасывает ошибку с переданным текстом, а также записывает ее в лог выполнения с типом `ERROR`. Пример: ```scala tst.raise("Текст с ошибкой"); ``` ##### tst.assertTrue Проверят, что переданное условие истинно. Если оно ложно, то прекращает выполнение теста и записывает в лог выполнения ошибку. Если выполняется группа тестов, и проверка не пройдена то переходит к следующему тесту. Пример: ```scala tst.assertTrue(1 == 2); //запишет в лог "assertTrue не прошло проверку" tst.assertTrue(1 == 2, "Число 1 не равно числу 2"); //запишет в лог переданный текст "Число 1 не равно числу 2" ``` ##### tst.shouldBeTrue Проверят, что переданное условие истинно. Если оно ложно, то записывает в лог выполнения ошибку. Пример: ```scala tst.shouldBeTrue(1 == 2); //запишет в лог "shouldBeTrue не прошло проверку" tst.shouldBeTrue(1 == 2, "Число 1 не равно числу 2"); //запишет в лог переданный текст "Число 1 не равно числу 2" ``` ##### tst.sqlRows Возвращает количество строк, полученных в результате выполнения переданного запроса Пример: ```scala 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"}]} ```