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

Unit-тестирование используется для проверки отдельных функций, методов и классов исходного кода. В Global ERP такие тесты реализуются на основе ScalaTest и позволяют быстро проверять алгоритмы, валидацию данных, обработку исключений и бизнес-правила.

Документ описывает создание unit-тестов, выбор базового класса, запуск тестов, использование assert-методов и matchers.

Совет

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

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

В Global ERP unit-тесты могут применяться для проверки:

  • функций и методов;

  • API-классов;

  • Pkg-методов при наличии тестового контекста;

  • валидаторов;

  • парсеров;

  • преобразователей данных;

  • расчетных алгоритмов;

  • вспомогательных классов и утилит.

Перед созданием unit-теста необходимо определить:

  • какой класс или метод проверяется;

  • какие входные данные нужны;

  • какой результат считается ожидаемым;

  • нужен ли контекст бизнес-логики;

  • нужен ли доступ к базе данных;

  • требуется ли откат транзакции после теста.

Если тесту не нужны Api, Pkg и подключение к БД, используйте LangFunSuite. Если тест работает с прикладной бизнес-логикой или требует доступа к БД, используйте ApiTest.

Подготовка тестирования#

Перед созданием теста подготовьте:

  • модуль, в котором будет размещен тест;

  • пакет для тестового класса;

  • набор входных данных;

  • ожидаемый результат;

  • данные для очистки или отката, если тест изменяет БД.

Тесты, работающие с БД, должны выполнять откат транзакции или удалять созданные данные.

Создание и настройка#

Для unit-теста создание и настройка сводятся к созданию тестового класса в нужном модуле и выбору базового класса.

Чтобы создать тестовый класс:

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

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

  3. Перейдите в папку с исходными кодами:

    [module_name]/src/test/scala

    Совет

    Создать недостающую папку можно из контекстного меню IntelliJ IDEA: New > Directory.

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

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

Пример простого тестового класса:

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

Запуск тестов#

Unit-тест можно запустить из IDE, из консоли sbt или в составе массового запуска.

При локальной разработке удобно запускать тест из IntelliJ IDEA через операции Run или Debug.

Чтобы запустить тест из контекстного меню:

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

  2. В контекстном меню выполните операцию 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-тесты обычно не проверяют:

  • полный пользовательский сценарий;

  • взаимодействие нескольких подсистем;

  • корректность интерфейса;

  • поведение системы под нагрузкой;

  • реальные внешние интеграции.