Pull to refresh

Модульное тестирование для приложений на платформе IBM Notes/Domino

Reading time6 min
Views3K
Коснусь темы, которая в мире Notes/Domino практически не известна — модульного (unit) тестирования, и представлю вашему вниманию инструмент на написания unit-тестов для Notes/Domino под язык LotusScript. Статья в большей степени рассчитана на тех, кто работает с платформой IBM Notes/Domino (Lotus Notes), ведет разработку с использованием языка LotusScript.

Немного терминологии, которая будет использоваться в статье:

  • Приложение — база данных IBM Notes/Domino или совокупность баз данных, если они представляют собой единую логическую единицу.
  • Клиент — толстый клиент, ПО IBM (Lotus) Notes.
  • Сервер — серверное ПО IBM Domino.

Начну с описания того, как выглядит разработка, чтобы каждый мог взглянуть на себя. Речь, в первую очередь, идет о написании приложений под толстый клиент с использованием @-формул и языка LotusScript. Оба языка скриптовые, а код можно писать практически везде: в событиях и действиях UI-объектов (таких, как формы и представления), в библиотеках (только для языка LotusScript), агентах и других элементах дизайна базы. К тому же, для каждого события и действия можно независимо выбрать язык, на котором писать. В итоге, если не иметь какой-то подход к организации кода, получается ужасная лапшеобразная каша из кода, разбросанного по всем мыслимым и немыслимым местам. А теперь добавим к этому большой зоопарк приложений, и, обычно, небольшой штат специалистов.

Мне был привит (за что очень благодарен) определенный подход к написанию кода, который я в дальнейшем развивал. В итоге получилась целая система, которая говорит не только о том, где писать, но и как писать. Если в кратце:

  • вся бизнес-логика пишется на языке LotusScript, использование @-формул минимально;
  • весь код находится в библиотеках, причем разделен на:

    • код, который может использоваться как на сервере, так и на клиенте;
    • код, который может использоваться исключительно на клиенте;

  • в визуальных элементах (формы, представления) используются только вызовы библиотечного кода, причем, в подавляющем большинстве — однострочные;
  • максимальное использование объектных и минимальное использование процедурных возможностей языка LotusScript.

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

Товарищ google, ассистировавший при поиске средств именно модульного (unit) тестирования для IBM Notes/Domino под язык LotusScript, привел только к одному очень старому варианту проекта на OpenNTF.org. В целом, идея, которая крутилась у меня голове, была похожей, но реализация обещала несколько больше. Поэтому было принято решение писать свой, не побоюсь этого слова, framework для unit-тестирования с блэкджеком и куртизанками достаточно гибкий и расширяемый.

За основу построения логики тестов взял обычную для любого (наверно) фреймворка для unit-тестирования: класс с методами-тестами. Эти тесты надо собрать в кучу и прогнать. Итак, в голове составился некоторый список того, что нужно для минимального набора:

  1. тесты будут писаться в виде методов класса;
  2. тесты будет запускать агент;
  3. при написании тестов понадобится набор проверок (ассертов, assert);
  4. в рамках запуска тестов должна вестись статистика успешности теста и выводиться строка в тесте, где произошла ошибка.

Некоторые аспекты и допущения, которые были сделаны при проектировании реализации:

  • LotusScript не имеет в своем арсенале ни рефлексии, ни аннотаций, а значит необходимо будет явно указывать какие методы являются тестовыми. Варианты, например, с парсингом библиотеки с классами-тестами отмел, как минимум для первого этапа.
  • Минимальный набор проверок:

    • сравнение выражения с истиной;
    • ложью;
    • сравнение на равенство. Т.к. вопрос равенства — вещь контекстно-зависимая, в метод сравнения на равенство, будет передаваться объект. Класс этого объекта должен быть унаследован от класса-интерфейса (в языке LotusScript нет чистых интерфейсов) с одним публичными методом, возвращающим логическое значение;
    • проверка на ошибку, а именно, на код ошибки.

При реализации встал вопрос: как фиксировать статистику? Опять же, средств рефлексии и аннотаций нет, значит:

  • либо оборачивать каждый тестовый метод в агенте, который их запускает, кодом со статистикой;
  • либо внести это в сам тестовый метод.

Принял решение пойти по второму пути. В итоге родился класс AbstractTest с двумя публичными методами: BeginTest(ИМЯ_ТЕСТА), EndTest(ИМЯ_ТЕСТА). Почему публичными? Это было сделано для удобства, из-за особенностей встроенной IDE на основе Eclipse: приватные методы суперкласса видны потомкам и доступны для переопределения, но в подсказки самой IDE не попадают. Шаблон каждого тестового метода для успешного сценария выглядит вот так:

Public Sub TestMethod()
On Error GoTo ErrorHandler
    Const FuncName = "TestClassName.TestSubName ()"
    
    Call Me.BeginTest(funcName)
		
    'Код тестового метода располагается здесь
    
    Call Me.EndTest(funcName)
		
    GoTo endh
ErrorHandler:
    Error Err, "(" & DESIGN & ") " & FuncName & ", line " & Erl & Chr(10) & Error$
endh:
End Sub

Для тестовых методов с ожидаемой ошибкой он немного отличается и выглядит так:

Public Sub TestError()
On Error GoTo ErrorHandler
    Const FuncName = "DemoTest.TestAssertErrorExample ()"
		
    Call Me.BeginTest(funcName)
		
    'Код может быть как здесь
    
    On Error ERROR_CODE_TO_TEST GoTo AssertErrorHandler
    'Код, который завершается с ошибкой, обозначенной выше располагается здесь
		
    GoTo endh
AssertErrorHandler:
    Call Me.EndTest(funcName)
    Resume endh
ErrorHandler:
    Error Err, "(" & DESIGN & ") " & FuncName & ", line " & Erl & Chr(10) & Error$
endh:
End Sub

Из этих размышлений родился тестовый контекст, хранящий информацию времени выполнения тестов:

  • данные о статусах прохождения тестов;
  • возможность добавления тестов и изменения статусов;
  • ведение логирования.

Шаблонизированию подвергся и класс, ответственный за запуск тестов. Если свести к минимуму код, то он состоит из основного метода Run() и метода, который следует переопределить в агенте, запускающем тесты DoRunTests(). Метод DoRunTests как раз должен содержать создание классов-тестов и вызов соответствуютщих тестовых методов:

Class AbstractTestRunner
	
	Public Function Run()
		On Error GoTo ErrorHandler
		Const FuncName = "AbstractTestRunner.Run ()"
		[...]
		
		Call Me.DoRunTests()
		
		[...]
	
		GoTo endh
ErrorHandler:
		Error Err, "(" & DESIGN & ") " & FuncName & ", line " & Erl & Chr(10) & Error$
endh:
	End Function
	
	Private Sub DoRunTests()
		On Error GoTo ErrorHandler
		Const FuncName = "AbstractTestRunner.RunTests ()"
		
		'Override
		
		GoTo endh
ErrorHandler:
		Call Me.TestFailed(Error$)
		Resume Next
endh:
	End Sub
End Class

С проверками все просто. Небольшого отдельного внимания может заслуживать только метод сравнения на равенство:

Public Sub Equals(matcher As IMatcher)
	On Error GoTo ErrorHandler
	Const FuncName = "Assert.Equals ()"
		
	If Not Matcher.Matches() Then Call Me.Throwerror(funcName)
		
	GoTo endh
ErrorHandler:
	Error Err, "(" & DESIGN & ") " & FuncName & ", line " & Erl & Chr(10) & Error$
endh:
End Sub

Как говорилось выше, он принимает объект, чей класс унаследован от класса-интерфейса (IMatcher), имеющего только один публичный метод Matches() as boolean.

Class IMatcher
	Public Function Matches() As Boolean
	End Function
End Class

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

  • SimpleValueMatcher — простое сравнение 2ух значений. Значения принимаются с типом Variant.
  • DocumentItemsMatcher — сравнение заданных полей в двух заданных документах. Сравнение происходит на наличие поля, на тип и значение.
  • DatabaseMatcher — сравнение двух баз: id реплики, пути и названия. Суть — мы имеем дело с одной и той же базой. Целесообразно сюда еще сравнение на сервер добавить.
  • DocumentMatcher — сравнение 2х документов на идентичность: лежат в одной базе, имеют один и тот же UniversalId, совпадают по всем полям.

Проект размещен в открытом доступе на GitHub, распространяется по лицензии Apache 2.0 и снабжен подробной документацией по установке и использованию. Также проект был размещен на самой популярной для domino-водов площадке — Openntf.org.

В качестве развития вижу:

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

Так получилось, что та система, которая показала всю необходимость создания unit-тестирования для Domino и сподвигла меня на написание представленного инструмента, на текущий момент так и не покрыта тестами. Однако готовлю еще один проект, который изначально должен стать публичными. Первая версия уже готова и на 100% покрыта тестами, созданными на описанном фреймворке. Статистику прохождения тестов собираюсь прикладывать к каждому релизу.

Конструктивные идеи, предложения и замечания всегда приветствуются!
Tags:
Hubs:
Total votes 5: ↑5 and ↓0+5
Comments9

Articles