Unit-тестирование#
Unit-тестирование используется для проверки отдельных функций, методов и классов исходного кода. В Global ERP такие тесты реализуются на основе ScalaTest и позволяют быстро проверять алгоритмы, валидацию данных, обработку исключений и бизнес-правила.
Документ описывает создание unit-тестов, выбор базового класса, запуск тестов, использование assert-методов и matchers.
Совет
Для дополнительной информации см. библиотеку unit-тестирования: ScalaTest.
Unit-тесты рекомендуется использовать для кода, который можно проверить изолированно или с минимальным набором зависимостей.
В Global ERP unit-тесты могут применяться для проверки:
функций и методов;
API-классов;
Pkg-методов при наличии тестового контекста;
валидаторов;
парсеров;
преобразователей данных;
расчетных алгоритмов;
вспомогательных классов и утилит.
Перед созданием unit-теста необходимо определить:
какой класс или метод проверяется;
какие входные данные нужны;
какой результат считается ожидаемым;
нужен ли контекст бизнес-логики;
нужен ли доступ к базе данных;
требуется ли откат транзакции после теста.
Если тесту не нужны Api, Pkg и подключение к БД, используйте LangFunSuite. Если тест работает с прикладной бизнес-логикой или требует доступа к БД, используйте ApiTest.
Подготовка тестирования#
Перед созданием теста подготовьте:
модуль, в котором будет размещен тест;
пакет для тестового класса;
набор входных данных;
ожидаемый результат;
данные для очистки или отката, если тест изменяет БД.
Тесты, работающие с БД, должны выполнять откат транзакции или удалять созданные данные.
Создание и настройка#
Для unit-теста создание и настройка сводятся к созданию тестового класса в нужном модуле и выбору базового класса.
Чтобы создать тестовый класс:
Перейдите в окно проекта.
Выберите целевой модуль.
Перейдите в папку с исходными кодами:
[module_name]/src/test/scalaСовет
Создать недостающую папку можно из контекстного меню IntelliJ IDEA: New > Directory.
Создайте пакет
ru.bitec.app.[module_name].Создайте тестовый класс.
Пример простого тестового класса:
class Lesson1Test extends LangFunSuite {
test("HelloWorld") {
println("hello world")
}
}
Запуск тестов#
Unit-тест можно запустить из IDE, из консоли sbt или в составе массового запуска.
При локальной разработке удобно запускать тест из IntelliJ IDEA через операции Run или Debug.
Чтобы запустить тест из контекстного меню:
Переведите курсор на декларацию функции или класса, если нужно запустить все тесты класса.
В контекстном меню выполните операцию Debug для запуска в режиме отладки или Run для обычного запуска.
Подробнее см. выполнение тестов в руководстве IntelliJ IDEA.
Для массового запуска используйте команды testOnly, test или testQuick, описанные в документе по массовому тестированию модулей.
Проверка результата#
Для проверки результата в ScalaTest используются assert-методы и matchers.
Проверка должна быть связана с ожидаемым поведением тестируемого метода. Если проверка может завершиться ошибкой, добавляйте поясняющий текст, чтобы по результатам запуска было понятно, какая часть сценария не прошла проверку.
Базовые assert-методы#
Трейт Assertions содержит основные методы для проверки предположений.
assert(condition: Boolean)#
Метод assert проверяет условие. Если условие возвращает true, метод завершается нормально. Если условие возвращает false, выбрасывается ошибка 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()
}
}
assertResult(expected: Any)(actual: Any)#
Метод assertResult сравнивает ожидаемое значение с фактическим.
Пример:
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)
}
assertThrows[T <: AnyRef](f: => Any)#
Метод assertThrows используется, когда нужно проверить, что тестируемый код выбрасывает ошибку ожидаемого типа.
Пример:
test("data.oaObjQty.isEmpty") {
val data = new Bs_DistrData()
assertThrows[AppException] {
Bs_DistributionPkg().distribQty(data)
}
}
assume(condition: Boolean)#
Метод assume аналогичен assert, но если условие не выполняется, тест не завершается ошибкой, а отменяется.
Пример:
assume(database.isAvailable, "База данных не доступна.")
assume(database.getAllUsers.count === 9)
intercept[T <: AnyRef](f: => Any)#
Метод intercept аналогичен assertThrows, но позволяет получить ошибку ожидаемого типа для дополнительной проверки.
Пример:
val vEx = intercept[AppException] {
_api.validateOnBeforeManualInsert(ropDuplicate)
}
assert(vEx.getMessage.startsWith("Ошибка. Уже существует запись с классом типа объектов:"))
Matchers#
Трейт Matchers позволяет использовать в тестах комбинаторы вроде shouldBe вместо обычного assert.
По умолчанию к ApiTest и LangFunSuite Matchers не подключен. Чтобы использовать его методы, добавьте Matchers в объявление класса:
class Bs_AccApiTest extends ApiTest with Matchers {
test("set level on insert") {
val rop = api.insert()
rop.get(_.nLevel) should ===(1.nn)
}
}
Для дополнительной информации см. руководство пользователя: matchers.
Проверка размера и длины#
Для проверки размера и длины используются выражения:
<проверяемый объект> should have length (<ожидаемое значение>);<проверяемый объект> should have size (<ожидаемое значение>).
Пример:
test("проверка размера массива") {
val result = getArgs()
result should have length (3)
}
Проверка строк#
Проверка строки на наличие подстроки:
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*)?""")
Сравнения больше, меньше, равно#
Для сравнения используются операторы <, >, <=, >=.
Пример:
one should be < (7)
one should be > (0)
one should be <= (7)
one should be >= (0)
Особенности unit-тестирования#
Для unit-тестов важно выбирать минимально необходимый уровень окружения. Чем меньше внешних зависимостей требуется тесту, тем быстрее он запускается и тем проще анализировать причину ошибки.
Базовые классы:
LangFunSuite— используется для тестов, которым не нужны контроллеры бизнес-логики. Такие тесты не могут использоватьApi,Pkgи по умолчанию не имеют подключения к базе данных.ApiTest— используется для тестов, которым нужен контекст автономной бизнес-логики. Такие тесты могут использоватьApiиPkg-объекты. При этом запуск теста становится медленнее из-за инициализации контекста.
Изоляция и повторяемость#
Unit-тест должен давать одинаковый результат при каждом запуске.
Рекомендуется:
не зависеть от данных, созданных вручную в тестовой БД;
создавать необходимые данные внутри теста;
использовать уникальные значения для тестовых объектов;
выполнять
session.rollback()в блокеfinally, если тест работает с БД;не зависеть от порядка запуска других тестов.
Интеграция в массовый запуск или CI/CD#
Unit-тесты можно включать в массовый запуск по классу, модулю или всему проекту. Для локальной проверки обычно используется testOnly, а для регрессионной проверки — массовый запуск набора тестов.
В CI/CD рекомендуется запускать быстрые unit-тесты после сборки или перед публикацией артефакта, чтобы обнаруживать ошибки в изолированной логике до запуска более тяжелых интеграционных и UI-проверок.
Типовые проблемы#
При разработке unit-тестов часто возникают следующие проблемы:
тест зависит от состояния тестовой БД;
тест не очищает созданные данные;
проверка не содержит понятного сообщения об ошибке;
тест проверяет слишком много несвязанных условий;
для простого теста используется
ApiTest, хотя достаточноLangFunSuite;тест зависит от порядка запуска других тестов.
Рекомендации#
При разработке unit-тестов рекомендуется:
проверять один сценарий в одном тесте;
давать тестам понятные названия;
использовать
LangFunSuite, если тесту не нужен контекст бизнес-логики;использовать
ApiTest, если тесту нужныApi,Pkgили доступ к БД;добавлять поясняющие сообщения в
assert;выполнять откат данных после теста;
не использовать промышленную БД и промышленные внешние сервисы.
Ограничения#
Unit-тестирование проверяет отдельные фрагменты кода, но не заменяет интеграционные, UI- и нагрузочные тесты.
Unit-тесты обычно не проверяют:
полный пользовательский сценарий;
взаимодействие нескольких подсистем;
корректность интерфейса;
поведение системы под нагрузкой;
реальные внешние интеграции.