Привет.
Я думаю, многие из вас слышали про такую штуку, как FitNesse. Это одна из технологий тестирования, где тесты создаются как wiki-разметка (т.е. каждый тест — это web страница), и потом запускаются на определенной технологии (Java, .Net, PowerShell и пр.)
В этой статье я расскажу про пример использования FitNesse для тестирования .Net приложения. И заодно покажу несколько приемов и трюков, чтобы сократить Ваше время разработки. Кстати, все эти технологии абсолютно бесплатны.
Как это выглядит
Как я сказал, все тесты в FitNesee выглядят как web-страницы. Весь тест можно представить как последовательность таблиц, причем каждая таблица содержит одну функцию. Но это теория, а на практике вызов функции выглядит так:
login as | Some User |
Сразу поясню, что хабровские таблички выглядят растянутыми. На деле FitNesse будет их сжимать до минимального размера — см. тут: http://fitnesse.org/FitNesse.UserGuide.TwoMinuteExample.
В этой табличке содержится вызов метода «loginAs», который получает на вход «Some User». То есть программист реализует функцию loginAs, и далее тестировщик может её использовать. Как Вы уже поняли, имя функции — это объединенный полужирный текст. Параметры — это обычный текст. Каждая ячейка содержит только один параметр. Также каждая ячейка содержит либо полужирный текст, либо обычный.
login history of user | Some User | is |
When | IP | |
05.07.2014 | 192.168.0.1 | |
06.07.2014 | 192.168.0.1 |
В этом случае запускается уже функция LoginHistoryOfUserIs с аргументом Some User. На выходе ожидается коллекция объектов, каждый из которых имеет свойства When и IP.
Абсолютно аналогично можно подавать на вход набор объектов (например, настройки — эта табличка с парами ключ/значение).
Итого, каждый тест — это последовательность подобных таблиц. Программист создает функции, тестировщик создает с их помощью тестовый сценарий. Естественно, общие куски кода можно выделять в отдельные файлы, поддерживаются шаблоны, можно определить блоки, которые будут запускаться перед каждым тестом и т. д. Базовый функционал достаточно богатый для того, чтобы тестировать приложения (по крайней мере, бизнес логику, отдельно от UI)
Как запускать
Как я сказал выше, FitNesse может запускать функции на разных технологиях и языках программирования. Выглядит это следующим образом:
- FitNesse разбирает wiki разметку, определяет, что необходимо запустить (какие web страницы)
- FitNesse запускает выбранный Runner на требуемом языке программирования и рассказывает ему, что и как надо запускать. Причем, передается только текст, никаких объектов здесь нет
- Runner уже знает, с какими классами он работает. Он разбирает табличку и вызывает функцию
В этой статье я буду описывать про работу на .Net платформе, поэтому все классы и функции будут из этого мира. Для того, чтобы определить Runner следует написать, во-первых, эти строчки:
!define COMMAND_PATTERN {%m %p}
!define TEST_RUNNER {..binarycurrentBuildNetRunner.Executable.exe}
!path ..binarytestBuildNetRunner.InternalTests.dll
Первая относится к Runner'у (в примере используется этот Runner). Она определяет, как FitNesse будет запускать Runner. На вход передаются все эти аргументы + системные переменные, но это уже детали.
Вторая строчка — это путь к Runner'у.
Третья строчка — это путь к dll'кам (их может быть несколько!), которые содержат ваше API. Всё просто.
А вот пример для другого Runner'а:
!define TEST_RUNNER {${working_directory}LibfitsharpRunner.exe}
!define COMMAND_PATTERN {%m -a ${working_directory}MyTests.dll.config -r fitnesse.fitserver.FitServer,${working_directory}Libfitsharpfit.dll -v %p}
!path ${working_directory}MyTests.dll
Как Вы видите, здесь я уже определил изначально переменную ${working_directory}. И дальше, в зависимости он её значений, будут запускаться Runner'ы из разных папок и, что самое важное, тесты находятся тоже в разных папках. Такая структура очень полезна на build платформе, где зачастую может быть несколько веток, для которых идут сборки и работают тесты.
Итак, чтобы заработала вся эта система:
1. Скачиваете и запускаете FitNesse
2. Скачиваете один из Runner'ов
3. Создаете свою библиотеку, которая ссылается на библиотеки Runner'а и реализует минимальный API
4. Создаете тест, который вызывает функции вашей библиотеки
5. Запускаете его
Всё, после этого у Вас уже есть небольшой тест, который делает хоть что-то. Дальше дело за малым — улучшать качество API, увеличивать количество тестов и запускать их в процессе сборки.
Как создавать тестовое API?
Как Вы видите выше, создавать тесты, использующие API, крайне просто: Вы просто вызываете функции, сравниваете значения, создаете бизнес-сценарий. Естественно, что для этого необходимо качественно создавать функции для запуска, чтобы происходила «эмуляция» бизнес-действий. Ведь по сути, интеграционный тест — это обыкновенный тестировщик, который работает 24/7 и умеет очень быстро кликать мышкой.
А вот с созданием тестового API есть свои сложности. В своих примерах я буду использовать этот Runner, так как он наиболее распространенный (о нем есть упоминание даже на сайте FitNesse).
Для начала следует понять, как работает этот самый fitSharp. А делает он следующие вещи:
- Определяет список dll, которые следует запустить
- Определяет конфигурационный файл, который следует использовать
- Создает новый Application Domain для тестов
- Последовательно запускает функции, о которых его просил FitNesse
Вам же требуется создать классы, которые содержат эти самые функции для запуска. Для этого:
- Создайте проект
- Добавьте этот NuGet пакет
- Создайте класс, который бы наследовал DoFixture. Назовите класс, например, MyTestFixture
- Создайте какой-нибудь метод, например, public void MyTest()
- Откройте FitNesse (я надеюсь, он у Вас уже скопирован и запущен )
- Создайте в нем новый тест
- Определите TEST_RUNNER, COMMAND_PATTERN, и !path примерно так, как я описал выше. Лучше всего используйте абсолютные пути, так для начала будет легче.
- Добавьте таблицу со всего одной ячейкой — [namespace].MyTestFixture. Wiki текст будет выглядеть как | ''!-[namespace класса MyTestFixture].MyTestFixture-!'' |. По сути это полное имя класса.
- Добавьте таблицу тоже с одной ячейкой — MyTest: | '''My Test''' | (кстати, регистр символов не важен)
- Запустите тест
Если не было никаких недоработок, то тест запустится. Итак, что же мы сделали:
- Магия с созданием классов. Дело в том, что изначально fitSharp находит только классы, которые наследуются от DoFixture. Далее он будет их создавать и запускать функции. Классы могут быть internal.
- Объяснение FitNesse'у, что и где мы будем запускать.
- Магия с Fixture по умолчанию. Дело в том, что fitSharp требует функции в стиле | имя класса | имя функции + аргументы |. Чаще всего все функции сложены в одном классе. Чтобы не писать его в начале каждый раз, можно его определить единожды сверху. Переопределить, к сожалению, не всегда можно (есть мнение, что это баг). И несколько тоже не определить.
- Запуск тестовой функции. С этим всё совсем просто: какая функция была, такую и запустили.
С простыми текстами более-менее понятно. Перейдем к сложным.
Сложные параметры
Рассмотрим функцию LoginAs(string userLogin). Более-менее ясно, что она должна делать по смыслу — она должна входить в некую систему, а дальше тестовая среда должна работать как бы от этого пользователя. Всё логично. Однако какой же параметр передать на вход? Вариант 1 — передать просто строчку, а потом уже разбирать её. Не очень удобно, так как если у нас есть две функции: login и Get Login History, то придется повторить процедуру парсинга и пр.
Хоть это и не документировано, но на деле есть очень просто решение: если Вы хотите принимать на вход класс, то у него должен быть публичный статический метод Parse(string arg). Который, естественно, вернет наш класс. Например, Int32 полностью подходит под определение (для структур это тоже работает). Пользуясь этой схемой, получаем неплохое разделение ответственности: наши тестовые методы будут работать с уже более-менее обработанными данными. И все исключения при разборе всё равно отобразятся пользователю.
Абсолютная аналогия происходит с методами, которые возвращают коллекции (см. выше). Они проверяются с помощью таблички, ну то есть тестировщик подает на вход ожидаемые данные, а Ваша функция должна вернуть правильную коллекцию. В этом случае, если Вы возвращаете класс, в котором все поля могут быть разобраны (ну т. е. все типы имеют метод Parse), то будет происходить не сравнение строк, а сравнение объектов. Которое уже полностью контролируется Вами.
Табличка на входе
Зачастую удобно добавлять в систему данные с помощью таблиц. Стандартный пример — это любой Mapping (например, настройки — это Map: name --> value). То есть по сути на вход передается таблица и нескольких колонок. Это тоже можно сделать в fitSharp. Для этого Вам потребуется создать такой метод:
public SetUpFixture SetSettingsForUserAs(User user). Название метода и аргументы могут быть любыми, а вот возвращаемое значение — именно SetUpFixture.
Далее реализуем класс —
internal sealed class MySettings : SetUpFixture
{
private readonly User user;
public MySettings(User user)
{
this.user = user;
}
public NameValue(string parameterName, string parameterValue)
{
}
}
И в итоге наша таблица будет выглядеть так:
Set Settings For User | habrauser | As |
Name | Value | |
First Name | John | |
Last Name | Doe |
Как произошел mapping колонок более-менее ясно. И аргументы теперь подаются в виде вот таких табличек.
Автоматический запуск
Итак, Вы создали тест. Или даже набор тестов — Test Suite. И они заработали в FitNesse. Теперь самое время начать запускать их автоматически.
Есть два основных способа для запуска:
- С помощью rest запроса выполнить тест, скачать xml (например,
http://myServer:8080/MyTests?suite&format=xml
). Результатом будет xml файл с информацией, что выполнялось, как отработало, время, результат и пр.
- Запустить еще одну копию FitNesse в режиме «выполниться и закрыться»: java.exe -jar «fitnesse-standalone.jar» -d D: -c «MyTests?suite&format=xml» -b «d:buildsTestsResults.xml». В этом случае FitNesse запустится, выполнит rest запрос сам у себя и, после ответа, закроется.
Второй способ работает всегда, то есть не требуется запущенного FitNesse, более того, можно держать работающий FitNesse и одновременно с этим выполнять тесты таким способом. Все результаты будут появляться в обоих FitNesse'ах.
Итог
Как итог, в этой статье я постарался быстро-быстро пробежаться по верхам и рассказать, как создать и запускать интеграционные тесты FitNesse. Чаще всего через эти шаги проходят методом проб и ошибок, однако если бы у меня год назад была бы эта статья, она бы сэкономила пару дней точно. Надеюсь, она поможет и вам.
Автор: imanushin