Инфраструктура для интегрированного тестирования ПО
— No, Take the Fit Challenge Instead
— OK
Fit (fit.c2.com) — Framework for Integrated Test — это одновременно подход к спецификации ПО и доступный способ задания тестовых данных для него.
В идеале использование Fit должно позволить полностью отказаться от написания тестов вручную: все, что для них нужно, может извлекаться из Fit-спецификации. Впрочем, сами авторы идеи не пошли так далеко в своих планах, поскольку в показанных на сайте сценариях применения он используется лишь для задания тестовых данных для предварительно запрограммированных тестов. Этот подход, конечно, более прост в реализации и универсальнее[1], что позволяет справиться с различными граничными случаями, которые на практике могут встречаться в большом разнообразии.
Коротко, идея Fit заключается в том, чтобы сделать спецификацию программы «исполняемой». Достигается это за счет того, что в самих документах спецификации содержатся таблицы определенного формата, в которых описаны привязки для тестов. Интерпретатор Fit должен уметь найти эти таблицы, инициализировать тесты данными из них и выполнить определенные действия (основным из которых является проверка соответствия полученных результатов от вызова функций ожидаемым). В результате некоторые ячейки таблиц — ячейки ожидаемых результатов — должны окрасится в определенные цвета, что будет индикатором для всех участников процесса разработки: заказчиков и разработчиков, — того, как работает проверяемая функциональность. Таким образом достигается максимально возможная интеграция спецификации к программе с самой программой, о преимуществах чего, наверное, не стоит говорить.
Fit опирается на 3 основных формата задания привязок:
колонки (Column fixture) представляют собой таблицу, в которой каждой колонке соответствует имя аргумента или функции; в строках таблицы записываются варианты значений соответствующих аргументов и ожидаемый результат выполнения функций. Простой пример (№ 1):
Column-example | |||||
a | b | +() | -() | *() | /() |
3 | 2 | 6 | 0 | 6 | 1.5 |
1 | 0 | 1 | 1 | 0 | error |
Row-example | human | ||||
name | gender | date-of-birth | height | weight | family |
«Alex» | M | «10.01.67» | 185 | 67.3 | #h((wife . «Tamara») (children . («Maria» «Nadezhda»))) |
«Nadezhda» | F | «9.12.02» | 132 | 41.0 | nil |
действия (Action fixture) — в этой таблице записывается последовательность выполнения операций из набора заранее предопределенных[3]. Предполагается, что привязка Действия будет в основном использоваться для описания и проверки взаимодействия с интерфейсом программы (будь то консоль, GUI или веб-страница). Поэтому 4 первичных операции названы в соответствии с логикой этого домена (хотя они и имеют более общий смысл, который раскрыт ниже):
- start object — все последующие операции перенаправляются в данный объект;
- enter method argument — вызов функции c указанным аргументом (если функция относится к классу выбранного (операцией start) объекта, то вызов соответствующего метода;
- press method — вызов функции без аргументов;
- check method value — проверка соответствия результата выполнения функции заданному значению.
В целом, операции привязки Действие должны соответствовать модели тройной диспетчиризации:
- первая колонка таблицы — для описания операции в рамках абстрактного пользовательского интерфейса;
- вторая — конкретная часть интерфейса (в виде объекта или функции), с которой происходит взаимодействие;
- третья — данные, которые должна интерпретировать сама программа.
Action-example | action | |
press | Font | |
check | *focus* | Font-dialog |
start | Font-dialog | |
check | font-size | |
enter | font-size | 11 |
check | font-size | 11 |
press | ok | |
check | *focus* | Main-window |
Критика блочного тестирования (Unit Testing)
Тестирование сейчас стало одним из краеугольных камней «промышленного» процесса разработки ПО, а TDD — разработка, движимая тестами — одним из неотъемлемых терминов, характеризующий процесс разработки, претендующий на современность. Однако, при всем внимании, уделяемом вопросам тестирования, на мой взгляд, эта область пока не достигла зрелости, как и не выработано компромиссное понимание (или ряд альтернативных пониманий) того, что из себя должна представлять инфраструктура для тестирования (Test Framework)[4].Что говорить, если в стандарте IEEE по блочному тестированию не указываются даже требования, которым должна удовлетворять стандартная реализация подобной инфраструктуры. А нужно еще учесть различия запросов при изменении масштабов организации разработчиков от индивидуального программиста к группе, к большой организации и, наконец, к распределенному глобально потенциально не ограниченному по колличеству участников коллективу разработчиков.
На основании своего, в основном, теоретического знакомства с Unit Test Framework’ами я пришел к выводу, что современный ad hoc стандарт реализации таких систем включает достаточно тривиальную функциональность:
- проверка результатов на удовлетворения некоторым произвольно выбранным предикатам;
- описание тестовых классов/функций, их наборов и иерархий, а также их исполнение;
- загрузка тестовых данных (fixtures) из текстовых файлов (например, XML или YAML формата);
- использование бутафорских объектов/функций (mocks, stubs);
- некоторые библиотеки пытаются решать задачу интеграции с IDE.
- тестирование на случайной выборке тестовых данных;
- тестирование под нагрузкой;
- механизмы расширения для создания доменно-специфических тестовых модулей[5];
- и др.
Вторая задача тестовой инфраструктуры — поддержка регрессионного тестирования, т.е. удостоверения того, что изменения программного кода не привели к поломке уже существуещей функциональности. На данный момент ее решение, как правило, полностью оставляют на откуп программистской команде на основе предположения о том, что ее легко реализовать на базе тривиальных функций тестовой инфраструктуры.
И, наконец, последнее направление использования программных тестов, которое не менее важно (и с которого, можно сказать, все начиналось) — проверка корректности работы программы[6]. Эта задача граничит с задачей отладки, поэтому полноценная тестовая инфраструктура должна предоставлять средства для автоматизации отладки, и здесь основное внимание должно уделяться вопросам фиксации результатов вычислений различными способами.
Таким образом, универсальная инфраструктура для тестирования программ должна отвечать таким требованиям:
- ее ядро поддерживает базовые операции для автоматизации проверки результатов выполнения функций и отладки, описания тестов (с возможностью ссылок на одни тесты из других), исполнения тестов, определяет интерфейс для подключения модулей расширения. Ядро должно дать программисту инструментарий для подстройки инфраструктуры для своих нужд, а также создания расширений, таких как наборы доменно-специфических тестовых функций;
- в качестве модулей расширения должны быть реализованы возможности:
- задания тестовых данных разных форматов;
- использования бутафорских объектов;
- тестирования на случайной выборке данных;
- тестирования под нагрузкой;
- автоматизации регрессионного тестирования;
- и др.
Реализация Fit на Lisp
Поддержка Fit реализована в качестве модуля расширения для библиотеки NUTS.Оба сценариями использования Fit для тестирования:
- полностью автоматические тесты;
- задание тестовых данных;
- с помощью функции file-runner, соответствующей Fit-утилите FileRunner на других платформах (но не требующей предварительного определения тестов);
- с помощью специализированных на типе привязки (колонки, строчки, действия, действия реального времени) методов родовой функции interpret-fit-xture, используемых в реализации Fit-специфичного метода на родовой функции load-fixture библиотеки NUTS. На основе этой функции также работает макро def-fittest, уточняющий макро deftest из NUTS.
- парсинг HTML-файлов для выделения таблиц Fit-привязок, а также вставка данных в заданные HTML-тэги (в этих таблицах);
- адаптирование строковых данных, полученных из Fit-привязок, к типам данных языка;
- создание механизма определения акторов для привязок типа Действие и задания базовых акторов;
- рефлексия для определения членов класса или, как это сделано в данной реализации, списка аргументов функций.
Во-первых, для адаптации строковых данных к типам языка я полностью положился на встроенный парсер Lisp (read), который умеет обрабатывать все базовые типы языка (которые предполагают возможность прочтения; кроме хэш-таблиц), а также имеет мощный механизм расширения в форме reader macros. Как раз такой макр я добавил для введения возможности парсинга хэш-таблиц:
; a reader syntax for hash table like alists: #h([:test (test 'eql)] (key . val)*)
(set-dispatch-macro-character ## #h
#'(lambda (stream subchar arg)
(declare (ignore subchar)
(ignore arg))
(let* ((sexp (read stream))
(test (unless (single sexp) (getf sexp :test)))
(kv-pairs (if test (cddr sexp) sexp))
(table (gensym)))
`(let ((,table (make-hash-table :test (or ,test 'eql))))
(mapcar #'(lambda (cons)
(setf (gethash (car cons) ,table)
(cdr cons)))
',kv-pairs)
,table))))
Что касается механизмов рефлексии языка, то здесь было использовано 2 функции:- во-первых, функция operator-arglist, реализованная в библиотеке swank, являющейся составной частью платформо-независимой среды разработки SLIME, позволяет получить список аргументов любой функции (в том числе, макро). Затем этот список обрабатывается функцией slice-&-dice-arglist для выделения различных типов аргументов (которые у Lisp функции включают: обычные параметры, опциональные (&optional), по ключевым словам (&key), остаточные (&rest) и ряд других);
- во-вторых, Fit-проверка на появления исключений поддерживается функцией class-subclasses, которая задействует мета-объектный протокол (MOP) Lisp для получения всех подклассов данного класса. Эта функция используется для того, чтобы учитывать возможность появления исключения, являющегося подклассом заданного в Fit привязке[7].
Функция get-tag использует механизм множественных возвращаемых значений (multiple-values) и выдает содержание заданного тэга, а также дополнительные значения параметров тэга и пары начальная/конечная позиция тэга в строке. Эти значения группируются в списки, если функция вызывается без опционального параметра number (порядковый номер), если же параметр задан, то не нахождение тэга с таким порядковым номером считается ошибкой. При этом в результате вызова функции возвращаются тэги одного уровня вложенности (т.е. для случая вложенности одноименных тэгов, как в «<div>text <div>other text</div></div>», выдается только внешний div), что вносит определенную регуляризацию в обработку HTML-документа.
Вот примерная Fit-спецификация этой функции[8] (Пример № 4):
get-tag | |||
tag | source | number | get-tag() |
«td» | "Table below:<br /> <table cellpadding="10″> <tr> <td>first cell</td> <td>second cell</td> </tr><tr> <td colspan=2>second row with not quite standard <td>inside</td></td> </tr> </table>« | («first cell» «second cell» «second row with not quite standard <td>inside</td>») ("" "" " colspan=2«) ((49 . 68) (68 . 88) (99 . 168)) | |
2 | «second row with not quite standard <td>inside</td>» " colspan=2″ (99 . 168) | ||
3 | tag-not-found | ||
«div» | «<div>text <div>other text</div></div>» | («text <div>other text</div>») («„) ((0 . 37)) | |
„hr“ | „<p>a <hr size=2> bcd</p>“ | “» " size=2″ (5 . 16) | |
«td» | "<div class="first"><a href="cnn.com">cnn.com</a></div>« | nil nil nil | |
«some clear text» | nil nil nil | ||
«!doctype» | «<!DOCTYPE HTML PUBLIC »-//W3C//DTD HTML 4.0 Transitional//EN"> <HTML> <HEAD> <META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8″> <TITLE>FIT Self-test</TITLE> </HEAD> <!— comments —> <BODY></BODY></HTML>" | 0 | «" «HTML PUBLIC \»-//W3C//DTD HTML 4.0 Transitional//EN\«" (0 . 59) |
«!—» | 0 | «" «comments» (181 . 197) | |
«hr» | «<pa <hr size=2> bcd />» | error | |
«<p?>a <hr size=2> bcd</p?>» | error | ||
«<p>a <hr size=2> bcd» | error |
Наконец, что касается механизма определения акторов, то для этого используется хеш-таблица, в которую акторы помещаются как анонимные функции, созданные с помощью утилиты def-actor, а доступ к ним производится с помощью get-actor.
Вообще, вся реализация Fit основывается на использовании замыканий анонимных функций, которые, будучи первоклассными объектами языка, передаются между функциями обработки привязок.
Вот, собственно, и весь краткий экскурс в реализацию Fit в среде Common Lisp, который оставляет за рамками решение трудностей, связанных с программной обработкой HTML (чему будет посвящена отдельная статья, предлагающая альтернативный выход), а также реализацию привязок типа Действия в реальном времени с использованием SBCL-специфичной библиотеки поддержки потоков sb-thread (для тестовых целей). Все это можно детально изучить в программном коде, находящемся на SourceFourge. В комплект также включены примеры и Fit-спецификация для низкоуровневых функций самой библиотеки (таких как get-tag).
Анализ результатов
Статья началась с того, что было принято предложение Джеймса Шора реализовать Fit на своем языке (в данном случае, Common Lisp).При этом Шором были указаны такие критерии оценки качества языка, должные проявиться при реализации, как:
- компактность;
- читабельность;
- поддерживаемость;
- прикольность.
- то, что это реальное ПО;
- необходимость задействования разнообразных сфер: парсинг, ввод-вывод, рефлексия, интеграция с кодом третьих сторон;
- то, что он проектировался, чтобы быть переносимым между реализациями;
- небольшой объем — несколько дней работы.
- Всего эта реализация занимает около 900 строк кода (+ около 100 составляет актуальная версия библиотеки NUTS). Конечно, сравнение с более зрелыми версиями, которые могут включать специфические расширения, не совсем корректно (поэтому не буду сравнивать с версией на Java). Но, например, довольно компактная версия на Ruby, реализующая только первый сценарий применения Fit, составляет около 1700 строк кода.
- О читабельности и поддерживаемости собственного кода судить трудно. Скажу лишь, что все интерфейсные операции, предполагающие возможность расширения реализованы в виде родовых функций (generic functions). Кроме того, стоит выделить применение macrolet’ов (локальных макров) в самой громоздкой и трудной для понимания части кода — алгоритме для парсинга HTML, — с помощью чего удалось четко разделить три линии вычислений: базовый алгоритм, «хождение» по дереву тэгов и формирование результата, — а также соблюдение принципа поддержки функционального интерфейса (On Lisp, гл. 3.3).
- Что касается задействования разнообразных сфер программирования при написании кода, то, в принципе во всех из них можно было бы положиться на существующие средства языка, предоставляемые либо в виде стандартных функций (как то: read, MOP), либо в виде переносимых библиотек (swank,
cl-ppcre). Единственная сфера, в которой я, как и многие другие разработчики Fit, создал собственное решение, является работа с HTML-текстом, наверное, потому, что это ключевая для работы всей Fit-инфраструктуры функция, которая должна быть достаточно гибкой (особенно, учитывая отсутствие единообразности в языке HTML, с которой постоянно нужно «бороться» при его автоматизированной обработке). - На реализацию Fit у меня ушло не несколько дней, как утверждает автор, а около 20 дней разной загружнности: от пары часов в день до полноценного рабочего дня. Впрочем, это было довольно прикольно...
Примечания
- ^ Поскольку он опирается не исключительно на четко заданные форматы привязок (fixtures). (Хотя, ничего не мешает вводить свои собственные форматы, охватывающие любые варианты задания привязок для тестов. Вопрос только в том, чтобы не перейти грань за которой «язык» задания привязок станет сравнимым по сложности с базовым языком, на котором пишется ПО и тесты)
- ^ В первую очередь, разумеется, пользовательских классов. Однако, если инфраструктура претендует на полноту охвата для данного языка в ней должны быть реализованы варианты инициализации всех потенциально необходимых структур (Например, для Lisp это также: struct, plist...)
- ^ Естественно, список предопределенных операций может быть расширен программистом. В представленной реалиации для этого предназначена функция
def-actor
- ^ В качестве зрелых областей, в которых существуют достаточно совершенные программные системы, можно назвать такие примеры из сферы поддержки разработки ПО, как:
- интегрированные среды разработки
- системы контроля версий программного кода
- системы отслеживания багов
- ^ Упрощенный пример такого модуля — функционал для тестирования http-ответов в Rails.
- ^ До определенной степени, конечно. В целом, принято разделять верификацию и тестирование программ как формальный и неформальный методы проверки ее корректности.
- ^ Пример применения: последняя строка из примера № 1, где в результате деления на 0 будет поднята ошибка
division-by-zero
, а в поле ожидаемого результата записано простоerror
- ^ В NUTS имена тестов отделены от других пространств имен, поэтому название теста идентичное имени функции не является ошибкой
10 коментарів
Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.