Идея статьи возникла после нескольких лекций о том, как писать тесты и как использовать xUnit. Обо всём можно по отдельности почитать подробно. Здесь же я собрал общую информацию о том, как удачно на практике всё это применяется и сопроводил ссылками, для дальнейшего ознакомления. Обзор делался по версии 2.0.

Соглашения в коде


Распространённая практика помещать все тестовые проекты в отдельный фолдер. Это относится и к структуре фолдеров на диске, и к фолдерам в солюшене. Практика распространена именно благодаря удобству. Так же, имя проекта и пространство имен полностью повторяют тестируемый модуль с добавлением в конце слова 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)


  1. EvilsInterrupt
    20.03.2016 12:46
    +1

    А почему это на Geektimes, вместо habrahabr?


    1. ETman
      20.03.2016 14:27

      Если действительно там должно, значит не сориентировался. Следующей статьей исправлюсь.


  1. Core2Duo
    20.03.2016 15:27
    +1

    То неловкое чувство, когда видишь статью для хабра на ГТ, а не наоборот.


  1. mbait
    21.03.2016 00:22
    +1

    … в отдельный фолдер

    Вроде такого?

    image


  1. AntonVasylenko
    22.03.2016 09:53

    TFS online — все отлично работает из коробки. screencast.com/t/V3dFd2fLvvnA + screencast.com/t/7CPU0F4xx + screencast.com/t/Yi6f5nq4fs22