Идея статьи возникла после нескольких лекций о том, как писать тесты и как использовать xUnit. Обо всём можно по отдельности почитать подробно. Здесь же я собрал общую информацию о том, как удачно на практике всё это применяется и сопроводил ссылками, для дальнейшего ознакомления. Обзор делался по версии 2.0.
Распространённая практика помещать все тестовые проекты в отдельный фолдер. Это относится и к структуре фолдеров на диске, и к фолдерам в солюшене. Практика распространена именно благодаря удобству. Так же, имя проекта и пространство имен полностью повторяют тестируемый модуль с добавлением в конце слова Tests (обычно через точку). Для простоты поиска тестов все тесты, относящиеся к некоторому классу помещаются в его класс с тестами. Т.о. получается пара из оргигинального класса и класса с тестами. Разделение между юнит, интеграционными, нагрузочными происходит либо по категорям, либо по логике основной системы. Это значит, что если интеграционные тест нельзя отнести к одному классу (обычно, это так), то они выносятся в отдельную сборку, посвященную тестируемому функционалу. Или, например, нагрузочные тесты могут тестировать работу некоторого метода одного класса. В этом случае тест размещается в парном классе. Структура самих тестов любого типа соответствует стилю AAA. Систематизируем вышесказанное:
xUnit является одним из популярных, нынче, фреймворков. На его описании останавливаться не буду, а приведу сухую выжимку. Если будут вопросы, пишите в коментах, отвечу.
Немного устаревшая информация, тем не менее, довольно полная, приведена в сравнении xUnit с другими фреймворками (MSTest, NUnit). Хочу отметить только важные отличия:
В остальном возможности похожи. xUnit приписывают очень высокую гибкость по кастомизации, возможность расширить/изменить поведение, запуск тестов и пр. Но мне не приходилось на практике что-то такое делать.
Fact — это отдельный юнит-тест, не принимающий параметров. Theory — это тест, принимающий параметры, при этом может быть несколько сценариев. Fixture — класс для настройки и очистки некоторого контекста. Контекст присоединяется к классу с тестами либо с помощью интерфейса IClassFixture, либо с помощью коллекции и интерфейса ICollectionFixture. Коллеция может включать несколько классов с тестами.
Пример Fact и двух вариантов Theory:
Чуть больше деталей тут и тут.
По умолчанию, тесты в одном классе и коллекции выполняются синхронно. Например, если при выполнении тестов у вас используется расшаренный ресурс и это вызывает проблемы (например, тесты падают, т.к. ресурс залочен), то можно поместить такие тесты в одну коллекцию и проблема уйдет, т.к. исчезнет параллелизм. В целом, стек вызовов и то, что происходит выглядит, примерно, так:
Стоит отметить, что при определении коллекции используется класс-маркер, который не создаётся вообще. Создаются только классы, присоединенные с помощью ICollectionFixture. Об этом подробнее читайте тут, а о параллелизме — тут.
Она есть, правда, не везде из коробки. В моём любимом TeamCity всё отлично, т.к. делать ничего не надо. Хуже в Jenkins, т.к. надо заморочиться с установкой плагина. А вот в TFS темный лес. Мне не удалось найти вменяемого примера установки и запуска xUnit. Буду рад ссылкам. Остается запуск силами скриптов MSBuild и NAnt.
Соглашения в коде
Распространённая практика помещать все тестовые проекты в отдельный фолдер. Это относится и к структуре фолдеров на диске, и к фолдерам в солюшене. Практика распространена именно благодаря удобству. Так же, имя проекта и пространство имен полностью повторяют тестируемый модуль с добавлением в конце слова Tests (обычно через точку). Для простоты поиска тестов все тесты, относящиеся к некоторому классу помещаются в его класс с тестами. Т.о. получается пара из оргигинального класса и класса с тестами. Разделение между юнит, интеграционными, нагрузочными происходит либо по категорям, либо по логике основной системы. Это значит, что если интеграционные тест нельзя отнести к одному классу (обычно, это так), то они выносятся в отдельную сборку, посвященную тестируемому функционалу. Или, например, нагрузочные тесты могут тестировать работу некоторого метода одного класса. В этом случае тест размещается в парном классе. Структура самих тестов любого типа соответствует стилю AAA. Систематизируем вышесказанное:
- Расположение проекта: в фолдере Tests
- Имя тестового проекта: [ProjectName].Tests
- Пространство имен: [Namespace].Tests
- Имя класса с тестами: [Class]Tests
- Пара 1-1 из тестируемого класса и класса с тестами
- Наименование юнит-тестов: BDD (мануал)
- Стиль тестов: AAA (Arrange-Act-Assert)
Сравнение xUnit с другим фреймворками
xUnit является одним из популярных, нынче, фреймворков. На его описании останавливаться не буду, а приведу сухую выжимку. Если будут вопросы, пишите в коментах, отвечу.
Немного устаревшая информация, тем не менее, довольно полная, приведена в сравнении xUnit с другими фреймворками (MSTest, NUnit). Хочу отметить только важные отличия:
- Проверка исключений делается ассертами, вместо атрибутов, что больше соответствует стилю AAA (Assert.Throws, Record.Exception). Внутри исключения ловятся try-catch блоком.
- Замена специальных атрибутов естественными возможностями языка (конструктор, IDisposable, IClassFixture, ICollectionFixture)
В остальном возможности похожи. xUnit приписывают очень высокую гибкость по кастомизации, возможность расширить/изменить поведение, запуск тестов и пр. Но мне не приходилось на практике что-то такое делать.
Fact, Theory и другие понятия
Fact — это отдельный юнит-тест, не принимающий параметров. Theory — это тест, принимающий параметры, при этом может быть несколько сценариев. Fixture — класс для настройки и очистки некоторого контекста. Контекст присоединяется к классу с тестами либо с помощью интерфейса IClassFixture, либо с помощью коллекции и интерфейса ICollectionFixture. Коллеция может включать несколько классов с тестами.
Пример Fact и двух вариантов Theory:
public class TestSuite
{
[Fact]
public void Should_do_somthing(){...}
[Theory]
[InlineData(20, 180, 80, ”good”)]
[InlineData(20, 180, 50, ”bad”)]
public void Should_measure_weight(int age, int height, decimal weight, string expected){...}
[Theory]
[MemberData(“AgeHeightWeightData”)]
public void Should_measure_weight(int age, int height, decimal weight, string expected){...}
public static IEnumerable<object[]> AgeHeightWeightData()
{
yield return new object[] {20, 180, 80, "good"};
yield return new object[] {20, 180, 50, "bad"};
}
}
Чуть больше деталей тут и тут.
Контексты и выполнение тестов
По умолчанию, тесты в одном классе и коллекции выполняются синхронно. Например, если при выполнении тестов у вас используется расшаренный ресурс и это вызывает проблемы (например, тесты падают, т.к. ресурс залочен), то можно поместить такие тесты в одну коллекцию и проблема уйдет, т.к. исчезнет параллелизм. В целом, стек вызовов и то, что происходит выглядит, примерно, так:
CollectionFixture: ctor - переменные этого контекста видны классам TestClass1 и TestClass2
ClassFixture: ctor - переменные этого контекста видны в TestClass1
TestClass1: ctor - переменные этого контекста видны только в одном тесте
Test1() - выполняется синхронно внутри одного класса и коллекции
TestClass1: disposed - все переменные контекста удаляются
TestClass1: ctor
Test2()
TestClass1: disposed
ClassFixture: disposed - все переменные контекста удаляются
TestClass2: ctor
Test3()
TestClass2: disposed
CollectionFixture: disposed
TestClass3: Test4() - этот тестовый класс не использует другие контексты и тесты выполняются параллельно с остальными
Стоит отметить, что при определении коллекции используется класс-маркер, который не создаётся вообще. Создаются только классы, присоединенные с помощью ICollectionFixture. Об этом подробнее читайте тут, а о параллелизме — тут.
Поддрежка CI
Она есть, правда, не везде из коробки. В моём любимом TeamCity всё отлично, т.к. делать ничего не надо. Хуже в Jenkins, т.к. надо заморочиться с установкой плагина. А вот в TFS темный лес. Мне не удалось найти вменяемого примера установки и запуска xUnit. Буду рад ссылкам. Остается запуск силами скриптов MSBuild и NAnt.
Комментарии (5)
AntonVasylenko
22.03.2016 09:53TFS online — все отлично работает из коробки. screencast.com/t/V3dFd2fLvvnA + screencast.com/t/7CPU0F4xx + screencast.com/t/Yi6f5nq4fs22
EvilsInterrupt
А почему это на Geektimes, вместо habrahabr?
ETman
Если действительно там должно, значит не сориентировался. Следующей статьей исправлюсь.