Некоторое время назад попался мне твит о том, что знакомый стал использовать новый тестовый опенсорсный фреймворк Fixie и очень этим доволен. Так, что даже решил исправить все тесты в своем проекте на новый движок. После такого, я просто не мог оставаться в стороне и даже не взглянуть, что это за зверь такой и чем он так радует окружающих.
Далее я хочу представить вам обзор фреймворка, его возможности, и понять, действительно ли это новое слово в тестировании, стоит к этому присмотреться или же можно пройти мимо и забыть.
Как сказано на самом сайте, Fixie – Conventional Testing for .NET. Т.е. тестирование по соглашению. Под соглашениями здесь понимается то, к чему мы в целом привыкли – все операции выполняются на основе «устного» договора, джентельменского соглашения об именовании. Ближайший пример – scaffolding. Это когда мы договорились, например, что тестовые классы содержат слово Test, или что тестовые классы должны быть публичными и ничего не возвращать. Тогда такие классы будут распознаны как тестовые. И больше никаких атрибутов и всего такого прочего. Просто классы и методы.
На первый взгляд всё это выглядит хорошо и даже радует. Получается, что необходимо только правильно называть методы и классы и будет счастье. Заодно можно натренировать команду называть классы тестовые как надо, а не произвольным сочетанием слов, как-то относящихся к теме тестируемого класса.
По умолчанию, Fixie настроен так, что тестовыми классами является всё, что оканчивается на Tests. Тестовыми методами – всё что внутри этих классов и не возвращает значения. Т.е. теоретически и, на самом деле практически, вот такой код уже будет распознаваться как тест:
Прежде чем двигаться дальше, стоит сказать пару слов об установке, и что необходимо для работы Fixie.
Команда nuget для установки самого фреймворка. Но на этом всё не заканчивается. Фреймворк не предоставляет встроенного способа для написания проверок данных в тесте. Для их использования вам необходимо воспользоваться одним из сторонних решений:
Это всё так же устанавливается с помощью NuGet. В примере выше использован пакет FluentAssertion, так как он лично мне больше нравится и большой разницы по сравнению с другими вариантами по сути нет.
На этом собственно всё, и можно счастливо и весело писать код. Если вы только начали свой путь в разработке и тестировании в частности. Если вы опытный человек, то возникает много вопросов, как эта штука настраивается и насколько удобно гонять тесты.
Кстати о запуске тестов. Автор честно признается, что ему надо было запускать тесты из консоли и поэтому он сделал интеграцию со студией в последнюю очередь. Есть плагин для ReSharper, но он для версий от 8.1 до 8.3, т.е. с новой версией вы пролетарий. Ради тестов мне не хотелось откатываться на 8ю версию, поэтому не могу сказать насколько это комфортно.
Интеграция со студией выполняется на уровне обнаружения тестов и запуска их. Т.е. никакой подсветки в редакторе не будет. Имеются ввиду спец.значки тестов.
Здесь, на мой взгляд, кроется потенциальное место ошибки. Очепятки. Не так-то сложно опечататься в слове “Tests”, что приведет к пропуску тестов. Визуально они никак не выделяются. Справедливости ради надо сказать, что такая ситуация редка и маловероятна, если вы будете использовать правильные средства студии и запускать не скопом все тесты, а, например, только те, что раньше не запускались.
В целом, можно пока что сослаться на молодость фреймворка и относительную неизвестность и, как следствие, отсутствие многих инструментов.
Чтобы два раза не возвращаться к теме обнаружения тестов и генерирования результатов документация нам сообщает, что Fixie может генерировать отчеты в стиле NUnit и xUnit, что значительно облегчит жизнь тем, у кого нормально построен CI.
Основная сила фреймворка в возможности гибкой настройки того, как ваши тесты будут идентифицироваться, как запускаться и как валидироваться и так далее. По умолчанию ничего с фреймворком не идет. Однако в репозитории находится много разнообразных примеров.
Чтобы все стало интереснее давайте немного покопаемся в том, как настроить фреймворк под свои нужды. Например, как сказать, какие еще классы должны идентифицироваться как тестовые.
Все настройки происходят в конструкторе класса, наследованного от Convention.
По умолчанию, настройки выглядят примерно следующим образом:
Код говорит о том, что тестовые классы должны заканчиваться на Tests и тестовым методом будет все что возвращает null. В целом годно.
Я стараюсь придерживаться принципа, когда тестовый класс начинается со слова When, а тесты со слов Then. Получается достаточно стройная картина в тестраннере и при написании тестов ты уже знаешь, что ты тестируешь, т.е. надо только подумать об эффектах для теста. Дополнительным бонусом является то, что тестовые классы получаются короткими и ответственность между тестами не смешивается.
Естественно, что любое правило лишь указывает на направление, а не является догмой. «A Foolish Consistency is the Hobgoblin of Little Minds» – поэтому всегда надо руководствоваться здравым смыслом.
Глядя на свой список тестов, я вижу, что не все классы заканчиваются на слово Tests или начинаются с When.
Строгой структуры именования нет и в тестах, в том смысле, что тесты должны начинаться со слова Should, например. Но на мой взгляд читая тесты прекрасно понимаешь, что там происходит. Это я говорю на основе опыта. Этот проект телится уже года 3-4 с переменным успехом и каждый раз я быстро и успешно вспоминаю что сделано, а что надо сделать. Это просто некоторая отдушина, когда бумажной работы становится слишком много.
В таком режиме именования, мне страшновато (да и лень) указывать ключевые слова по которым будут определены тестовые классы. Кроме того, подход When… Then… на практике означает, что есть настроечный метод, который запускается в начале каждого теста и в самом тесте либо проверяются результаты, либо как-то влияем на созданный объект. Т.е. надо явно будет пометить, или указать настроечный метод (SetUp).
Для реализации я могу явно вызывать настроечный метод в каждом тесте. Например, вот так:
Конечно, в реальной жизни, надо писать что-то информативнее чем SetUpEnvironment(), но идея понятна. При такой реализации будут переменные класса, которые будут сохранять свои значения между тестами – что легко может привести к зависимым тестам, если я забуду в каком-нибудь тесте прописать строку инициализации.
Fixie предлагает решение для этой ситуции. Вот оно:
Т.е. необходимо реализовать наследника CaseBehavior и там прописать все, что надо. Документация говорит, что реализация
Хорошо, но даже если так, то в таком подходе я вижу, что мне надо создавать многие варианты «DefaultConvention» и прописывать логику инициализации вдали от теста, при этом при запуске вы не будете даже знать, что такой контекст существует.
Нетрудно это продемонстрировать. Пусть DefaultConvention будет как в примере выше, тогда при запуске класса
Ничто не говорит мне, что есть SetUp/TearDown.
МАГИЯ!!! Вообще это интересно, но в данном случае нет. Я бы не хотел себе такую магию на проекте. Это совершенно противоречит тому, что тесты должны быть прозрачными. Даже если я в том же классе напишу расширение для CaseBehavior – это не решение, так как будет не очень очевидно где мне искать класс, в котором это все будет настроено. Заниматься постоянной копи-пастой тоже не выход.
Сравним:
Против:
Строчек кода как-то будет поменьше в случае с NUnit, да и SetUp из класса не потеряется в проекте.
Может быть я предвзят и неправильно пишу тесты, но… что-то я не уверен в этом. Еще не встречал кучи статей о том, что SetUp это зло. Т.е. все можно до абсурда довести и делать инициализацию в SetUp половины проекта, но это другой разговор.
Однако и на этот случай есть решение. Можно использовать самописные атрибуты для определения нужных частей теста, тестов и классов. Т.е. можно полностью эмулировать NUnit, xUnit в вашем проекте.
С помощью аттрибутов можно сделать
Приведу здесь небольшой пример создания категорий и запуск их.
И запуск тестов в зависимости от категории:
Fixie.Console.exe path/to/your/test/project.dll --include CategoryA
Можно много чего еще сказать про Fixie, приводить примеры решения тех или иных задач, но первое впечатление уже есть. Поэтому стоит закругляться.
Получается, что Fixie это метафреймворк для тестирования. Вы вольны построить какие угодно правила и возможности для тестирования, при этом строятся они достаточно просто, если честно. Вопрос только в целесообразности. А надо ли это всё делать? Немного отпугивает, конечно, отсутствие поддержи R#, и того, что я не вижу что тест – реально тест, и он оказался распознан фреймворком. В продакшене я бы не стал использовать, но для домашнего использования и как перспективное средство – Fixie очень интересен. По крайней мере, я точно вспомню его, если будет какая-то интересная и специфичная задача тестирования, которая будет трудно решаться стандартными средствами NUnit.
Далее я хочу представить вам обзор фреймворка, его возможности, и понять, действительно ли это новое слово в тестировании, стоит к этому присмотреться или же можно пройти мимо и забыть.
Как сказано на самом сайте, Fixie – Conventional Testing for .NET. Т.е. тестирование по соглашению. Под соглашениями здесь понимается то, к чему мы в целом привыкли – все операции выполняются на основе «устного» договора, джентельменского соглашения об именовании. Ближайший пример – scaffolding. Это когда мы договорились, например, что тестовые классы содержат слово Test, или что тестовые классы должны быть публичными и ничего не возвращать. Тогда такие классы будут распознаны как тестовые. И больше никаких атрибутов и всего такого прочего. Просто классы и методы.
На первый взгляд всё это выглядит хорошо и даже радует. Получается, что необходимо только правильно называть методы и классы и будет счастье. Заодно можно натренировать команду называть классы тестовые как надо, а не произвольным сочетанием слов, как-то относящихся к теме тестируемого класса.
Установка и первый тест
По умолчанию, Fixie настроен так, что тестовыми классами является всё, что оканчивается на Tests. Тестовыми методами – всё что внутри этих классов и не возвращает значения. Т.е. теоретически и, на самом деле практически, вот такой код уже будет распознаваться как тест:
public class SuperHeroTests {
public void NameShouldBeFilled() {
var superHero = new SuperHero();
superHero.Name
.Should()
.NotBeEmpty();
}
}
Прежде чем двигаться дальше, стоит сказать пару слов об установке, и что необходимо для работы Fixie.
Команда nuget для установки самого фреймворка. Но на этом всё не заканчивается. Фреймворк не предоставляет встроенного способа для написания проверок данных в тесте. Для их использования вам необходимо воспользоваться одним из сторонних решений:
- FluentAssertions
- Should
- Shouldly
- Что-то другое из этой темы
Это всё так же устанавливается с помощью NuGet. В примере выше использован пакет FluentAssertion, так как он лично мне больше нравится и большой разницы по сравнению с другими вариантами по сути нет.
На этом собственно всё, и можно счастливо и весело писать код. Если вы только начали свой путь в разработке и тестировании в частности. Если вы опытный человек, то возникает много вопросов, как эта штука настраивается и насколько удобно гонять тесты.
Интеграция
Кстати о запуске тестов. Автор честно признается, что ему надо было запускать тесты из консоли и поэтому он сделал интеграцию со студией в последнюю очередь. Есть плагин для ReSharper, но он для версий от 8.1 до 8.3, т.е. с новой версией вы пролетарий. Ради тестов мне не хотелось откатываться на 8ю версию, поэтому не могу сказать насколько это комфортно.
Интеграция со студией выполняется на уровне обнаружения тестов и запуска их. Т.е. никакой подсветки в редакторе не будет. Имеются ввиду спец.значки тестов.
Здесь, на мой взгляд, кроется потенциальное место ошибки. Очепятки. Не так-то сложно опечататься в слове “Tests”, что приведет к пропуску тестов. Визуально они никак не выделяются. Справедливости ради надо сказать, что такая ситуация редка и маловероятна, если вы будете использовать правильные средства студии и запускать не скопом все тесты, а, например, только те, что раньше не запускались.
В целом, можно пока что сослаться на молодость фреймворка и относительную неизвестность и, как следствие, отсутствие многих инструментов.
Чтобы два раза не возвращаться к теме обнаружения тестов и генерирования результатов документация нам сообщает, что Fixie может генерировать отчеты в стиле NUnit и xUnit, что значительно облегчит жизнь тем, у кого нормально построен CI.
Тюнинг
Основная сила фреймворка в возможности гибкой настройки того, как ваши тесты будут идентифицироваться, как запускаться и как валидироваться и так далее. По умолчанию ничего с фреймворком не идет. Однако в репозитории находится много разнообразных примеров.
Чтобы все стало интереснее давайте немного покопаемся в том, как настроить фреймворк под свои нужды. Например, как сказать, какие еще классы должны идентифицироваться как тестовые.
Все настройки происходят в конструкторе класса, наследованного от Convention.
public class CustomConvention : Convention {
public CustomConvention () {
}
}
По умолчанию, настройки выглядят примерно следующим образом:
public class DefaultConvention : Convention {
public DefaultConvention () {
Classes
.NameEndsWith("Tests");
Methods
.Where(method => method.IsVoid());
}
}
Код говорит о том, что тестовые классы должны заканчиваться на Tests и тестовым методом будет все что возвращает null. В целом годно.
Я стараюсь придерживаться принципа, когда тестовый класс начинается со слова When, а тесты со слов Then. Получается достаточно стройная картина в тестраннере и при написании тестов ты уже знаешь, что ты тестируешь, т.е. надо только подумать об эффектах для теста. Дополнительным бонусом является то, что тестовые классы получаются короткими и ответственность между тестами не смешивается.
Естественно, что любое правило лишь указывает на направление, а не является догмой. «A Foolish Consistency is the Hobgoblin of Little Minds» – поэтому всегда надо руководствоваться здравым смыслом.
Глядя на свой список тестов, я вижу, что не все классы заканчиваются на слово Tests или начинаются с When.
Строгой структуры именования нет и в тестах, в том смысле, что тесты должны начинаться со слова Should, например. Но на мой взгляд читая тесты прекрасно понимаешь, что там происходит. Это я говорю на основе опыта. Этот проект телится уже года 3-4 с переменным успехом и каждый раз я быстро и успешно вспоминаю что сделано, а что надо сделать. Это просто некоторая отдушина, когда бумажной работы становится слишком много.
В таком режиме именования, мне страшновато (да и лень) указывать ключевые слова по которым будут определены тестовые классы. Кроме того, подход When… Then… на практике означает, что есть настроечный метод, который запускается в начале каждого теста и в самом тесте либо проверяются результаты, либо как-то влияем на созданный объект. Т.е. надо явно будет пометить, или указать настроечный метод (SetUp).
Для реализации я могу явно вызывать настроечный метод в каждом тесте. Например, вот так:
public class SuperHeroTests {
public void NameShouldBeFilled() {
SetUpEnvironment();
var superHero = new SuperHero();
superHero.Name
.Should()
.NotBeEmpty();
}
}
Конечно, в реальной жизни, надо писать что-то информативнее чем SetUpEnvironment(), но идея понятна. При такой реализации будут переменные класса, которые будут сохранять свои значения между тестами – что легко может привести к зависимым тестам, если я забуду в каком-нибудь тесте прописать строку инициализации.
Fixie предлагает решение для этой ситуции. Вот оно:
public class DefaultConvention : Convention {
public DefaultConvention () {
Classes
.NameEndsWith("Tests");
Methods
.Where(method => method.IsVoid());
CaseExecution
.Wrap<HeroUniverseSetup>();
}
}
public class HeroUniverseSetup : CaseBehavior {
public void Execute(Case context, Action next) {
// реализация
}
}
Т.е. необходимо реализовать наследника CaseBehavior и там прописать все, что надо. Документация говорит, что реализация
- CaseBehavior = [SetUp] / [TearDown]
- FixtureExecution = [FixtureSetUp] / [FixtureTearDown]
Хорошо, но даже если так, то в таком подходе я вижу, что мне надо создавать многие варианты «DefaultConvention» и прописывать логику инициализации вдали от теста, при этом при запуске вы не будете даже знать, что такой контекст существует.
Нетрудно это продемонстрировать. Пусть DefaultConvention будет как в примере выше, тогда при запуске класса
public class SuperHeroTests {
public void NameShouldBeFilled() {
var superHero = new SuperHero();
superHero.Name
.Should()
.NotBeEmpty();
}
}
Ничто не говорит мне, что есть SetUp/TearDown.
МАГИЯ!!! Вообще это интересно, но в данном случае нет. Я бы не хотел себе такую магию на проекте. Это совершенно противоречит тому, что тесты должны быть прозрачными. Даже если я в том же классе напишу расширение для CaseBehavior – это не решение, так как будет не очень очевидно где мне искать класс, в котором это все будет настроено. Заниматься постоянной копи-пастой тоже не выход.
Сравним:
public class DefaultConvention : Convention {
public DefaultConvention () {
Classes
.InTheSameNamespaceAs(typeof(DefaultConvention))
.NameEndsWith("Tests");
Methods
.Where(method => method.IsVoid());
CaseExecution
.Wrap<HeroUniverseSetup>();
}
}
public class HeroUniverseSetup : CaseBehavior {
public void Execute(Case context, Action next) {
// реализация
}
}
public class SuperHeroTests {
public void NameShouldBeFilled() {
var superHero = new SuperHero();
superHero.Name
.Should()
.NotBeEmpty();
}
}
Против:
[TestFixture]
public class SuperHeroTests {
[SetUp]
public void Init() {
// реализация
}
[Test]
public void NameShouldBeFilled() {
var superHero = new SuperHero();
superHero.Name
.Should()
.NotBeEmpty();
}
}
Строчек кода как-то будет поменьше в случае с NUnit, да и SetUp из класса не потеряется в проекте.
Может быть я предвзят и неправильно пишу тесты, но… что-то я не уверен в этом. Еще не встречал кучи статей о том, что SetUp это зло. Т.е. все можно до абсурда довести и делать инициализацию в SetUp половины проекта, но это другой разговор.
Однако и на этот случай есть решение. Можно использовать самописные атрибуты для определения нужных частей теста, тестов и классов. Т.е. можно полностью эмулировать NUnit, xUnit в вашем проекте.
С помощью аттрибутов можно сделать
- поддержку категорий
- параметризованные тесты
- тесты, выполняющиеся в строгой очередности
- все что придет в голову
Приведу здесь небольшой пример создания категорий и запуск их.
public class CustomConvention : Convention
{
public CustomConvention()
{
var desiredCategories = Options["include"].ToArray();
var shouldRunAll = !desiredCategories.Any();
Classes
.InTheSameNamespaceAs(typeof(CustomConvention))
.NameEndsWith("Tests");
Methods
.Where(method => method.IsVoid())
.Where(method => shouldRunAll || MethodHasAnyDesiredCategory(method, desiredCategories));
if (!shouldRunAll)
{
Console.WriteLine("Categories: " + string.Join(", ", desiredCategories));
Console.WriteLine();
}
}
static bool MethodHasAnyDesiredCategory(MethodInfo method, string[] desiredCategories)
{
return Categories(method).Any(testCategory => desiredCategories.Contains(testCategory.Name));
}
static CategoryAttribute[] Categories(MethodInfo method)
{
return method.GetCustomAttributes<CategoryAttribute>(true).ToArray();
}
}
И запуск тестов в зависимости от категории:
Fixie.Console.exe path/to/your/test/project.dll --include CategoryA
Моё решение
Можно много чего еще сказать про Fixie, приводить примеры решения тех или иных задач, но первое впечатление уже есть. Поэтому стоит закругляться.
Получается, что Fixie это метафреймворк для тестирования. Вы вольны построить какие угодно правила и возможности для тестирования, при этом строятся они достаточно просто, если честно. Вопрос только в целесообразности. А надо ли это всё делать? Немного отпугивает, конечно, отсутствие поддержи R#, и того, что я не вижу что тест – реально тест, и он оказался распознан фреймворком. В продакшене я бы не стал использовать, но для домашнего использования и как перспективное средство – Fixie очень интересен. По крайней мере, я точно вспомню его, если будет какая-то интересная и специфичная задача тестирования, которая будет трудно решаться стандартными средствами NUnit.