вторник, 26 апреля 2016 г.

Что мне не нравится в Xunit

xUnit - это самый популярный нынче тестовый фреймворк, который сейчас активно используется во многих open source проектах типа Roslyn, CoreFx и сотнях проектах поменьше. Я всю свою сознательную жизнь использовал NUnit, но сейчас, иногда по работе, иногда просто при работе с чужеродными кодовыми базами, мне приходится иметь дело с xUnit.

Поскольку тестовый фреймворк – это не особый рокет сайенс, то столкнуться со сложностями при его использовании вообще довольно сложно. Конечно, если это не MSTest, который вызывает состояние недоумения с завидным постоянством.

Но несмотря на зрелость и всеобщую популярность, иногда таки посещают мысли вида WAT при работе с xUnit. Есть два конкретных момента, которые вгоняют меня в легкий ступор. Возможно, ступор этот обусловлен небольшим опытом работы с этим чудом, или неумением его готовить, но все же.

Итак, начнем.

 

1. Отсутствие годной документации

Нет, ну серьезно. Попробуйте найти официальную информацию о параметризованных тестах, о других атрибутах или о модели расширения (вот это хз, я не искал, просто добавил ради убедительности своих аргументов;)).

Поскольку я пользуюсь параметризованными тестами довольно часто, то был удивлен, что гугл как-то не сильно знает толковых материалов по этой теме. Или, например, попробуйте найти информацию о том, что такое CollectionDefinitionAttribute и с чем его едят. Не просто скудный пример из официальной документации, а что-то вменяемое.

Подобное отношение к документации для любого open source проекта весьма объяснимо, но для столь популярного тестового фреймворка такое отношение немного странно.

2. Отсутствие возможности указать сообщение в утверждении

Собственно, это главная проблема с моей точки зрения.

В любом тестовом фреймворке есть возможность указать кастомное сообщение в методах Assert, в любом, но не в xUnit. Точнее, не так. Вы можете это сделать только в двух случаях – в методах Assert.True и Assert.False:

// Декларация метода
string method = @"public void Foo() {}";
var contractBlock = ContractBlock.GetContractBlockFor(method);
// Нет возможности указать сообщение
Assert.NotNull(contractBlock);
// Можно только так
Assert.True(contractBlock != null, "Contract block should not be null");

Таким образом, сейчас вы сталкиваетесь перед выбором: хотите ли вы увидеть информативное сообщение о нарушенном утверждении (типа ждали 1 получили 42), или вы хотите видеть свое кастомное сообщение но со стандартным префиксом «ждали true, получили false». И ни в одном из этих случаев вы не получите (ИМХО) того, что хотите, т.е. всей информации целиком:

clip_image001

Авторам xUnit несколько раз «говорили» о том, что было бы здорово иметь перегрузку методов Assert с указанием сообщений, на что был получен следующий ответ:

We are a believer in self-documenting code; that includes your assertions. If you cannot read the assertion and understand what you're asserting and why, then the code needs to be made clearer. Assertions with messages are like giving up on clear code in favor of comments, and with all the requisite danger: if you change the assert but not the message, then it leads you astray.

Взято из: https://github.com/xunit/xunit/issues/350

Мне эта логика абсолютно не понятна. Возможно, авторы xUnit хотят пойти еще дальше и запретить использовать более одного утверждения в тесте? Да, тестировать сразу 50 вещей одновременно – это плохо, но вполне нормально, что для тестирования одного аспекта вам понадобится более одного вызова метода Assert.

Давайте рассмотрим мой пример: когда утверждение упадет, то разработчик (т.е. я) сможет понять, что же пошло не так, глядя лишь на сообщение об ошибке. Мне не нужно будет переходить к коду, мне не нужно знать строку, где произошла ошибка, я сразу же могу фиксить код.

Автор xUnit считает, что в сообщении может указываться лишь информация, которая и так продублирована в самом утверждении, но это не так. Я использую сообщения в методах Assert не только для документации кода, но и для вывода важной информации о состоянии системы, которая может быть использована прямо сейчас для диагностики проблемы!

Вот пример, когда такой подход очень полезен.

Есть довольно популярный паттерн обработки ошибок под названием Notification Pattern. Идея его в том, что если «ошибка» является чем-то ожидаемым, то вместо генерации исключений нужно возвращать некоторый кастомный объект, который и будет содержать информацию о проблеме (или проблемах). Этот подход применим довольно часто, например, для валидации данных на UI, валидации во время парсинга входных данных (например, JSON или XML данных) или вывода сообщений ошибок компиляции.

Вот пример из тестовой инфраструктуры проекта ErrorProne.NET: метод NoDiagnostic из класса AnalyzerTestFixture проверяет, что в процессе парсинга кода не было ни одной диагностики, а если же они были, то все эти диагностики выводятся в сообщении об ошибке:

protected void NoDiagnostic(     Document document, string diagnosticId, ProcessedCode processedDocument)
{     var diagnostics = GetDiagnostics(document);          string diagnosticMessage = string.Join("\r\n", diagnostics.Select(d => d.ToString()));     Assert.That(diagnostics.Count(d => d.Id == diagnosticId),          Is.EqualTo(0), $"Expected no diagnostics, but got some:\r\n{diagnosticMessage}");
}

Теперь, если один из тестов будет падать из-за наличия предупреждений компилятора или других диагностик, то вместо малоинформативного сообщения “Expected 0 but got 1”, пользователь сможет увидеть текст полученной диагностики на экране:

clip_image002

Подобный подход позволяет оставаться в контексте решаемой задачи и приступать к фиксу кода лишь после беглого взгляда на output работы тестов. При работе с xUnit я заметил, что отсутствие необходимой информации приводит к необходимости запуска теста в режиме отладки, что серьезно сбивает процесс «сделал – написал тест – запустил – увидел проблему – пофиксил».

Сообщение в утверждениях – это не просто дублирование информации, оно может содержать важную информации о входных данных или состоянии тестируемого класса!

Заключение

Xunit – это очень толковый и зрелый тул. Он умеет 100 500 разных штук, но, ИМХО, проваливается на базовом кейсе. Особенно настораживает отношение авторов библиотеки к фидбеку пользователей: тикет, который я приводил выше, заморожен и вы не можете добавлять туда комментарии. А любой новый, я почему-то уверен, будет закрыт, как дубликат. И я бы понял принципиальность авторов, если бы речь шла о действительно важных вещах, или о вещах, которые будут провоцировать плохой код: но я искренне не понимаю, какой вред может нанести дополнительный аргумент в методах Assert, который явно бывает полезен в ряде сценариев.

З.Ы. А вы каким тестовым фреймворком пользуетесь и что в нем вам в нем нравится/не нравится?

25 комментариев:

  1. Еще одно забавное наблюдение: есть Assert.Equal(string, string, ignoreCase), но нет Assert.NotEqual(string, string, ignoreCase). Странно:)

    ОтветитьУдалить
  2. На работе заставляют использовать NUnit,но большая часть команды за переход на xUnit. В домашних проектах только xUnit.

    ОтветитьУдалить
    Ответы
    1. Леш, просто любопытно: а что тебе в xUnit-е существенно больше нравится по сравнению с NUnit-ом?

      Удалить
    2. Думаю моя любовь к нему объяснима несколькими фактами. Первый, и наверно наиболее весомый, это то, что xUnit исторически был первый тест фреймворк, с которым я начал плотно работать. Второе, список фактов, которые авторы приводят на своей странице Why Did we Build xUnit (http://xunit.github.io/docs/why-did-we-build-xunit-1.0.html) - прочитал их ещё когда был юн и зелен и мне они показались крайне убедительными. Из того что сходу вспоминается из приятных мелочей это IMHO чуть более удбное API: использование конструктора и IDisposable вместо SetUp и TearDown, NotEqual вместо AreNotEqual, отсутствие Greater в пользу ">" и т.п. Так же по ссылке меня убедила политика "Single Object Instance per Test Method". Ну вот и всё, из того что сходу вспоминается.
      По факту, отличия не суппер важные, но даже если предпочтения 51% против 49%, то чего бы и не получить 2% for free?
      PS: я за любой тест фреймворк, если это не MSTest!

      Удалить
  3. Используем стандартный студийный от MS.
    По теме согласен на 100%. Тоже самое, что писать код без комментариев. Ведь "код должен быть самодокументируемым" и его суть должна читаться из него же. Ага, когда в команде 10+ человек, и каждый вносит свою лепту (в тесты в том числе), остальные 9 должны прям сразу понять, из-за чего код не работает (возвращаясь от аналогии к тестам - прям сразу поймут что тест проверяет и какой надо сделать вывод).

    ОтветитьУдалить
    Ответы
    1. Уффф. Для меня главная проблема в МСтесте - отсутствие параметризованных тестов.

      Удалить
    2. Аналогично - в основном MSTest. Причина - работает интеграция с VS Express. Точнее "почти работает" - в 15ой студии они немного отломали переход к коду (

      По сути темы - также поддерживаю. Не понимаю, почему я должен видеть "Expected 2, got 1" вместо "Number of ears does not match. Expected 2, got 1.".

      У нас один из основных юз-кейсов для тестов - тестирование функций парсинга. Например - адреса. На вход получаем строку, на выходе - куча компонент адреса - регион, город, улица, и т.д. Соответственно, каждая компонента сравнивается своим ассертом.

      Хорошо, что в MSTest с этим все хорошо, поэтому в каждом ассерте у нас написано, какую компоненту мы сравнивали. Так что я сразу вижу что отломалось уже в сообщении об ошибке, а не лезу в код искать среди десяти одинаковых строк с ассертами.

      Удалить
    3. Михаил, а ведь с тест адапатерами в экспрессе должны работать и Nunit, и Xunit. Попробуйте.

      Удалить
  4. Лично мне очень понравилась идея тестового фреймворка Fixie (https://github.com/fixie/fixie). Какие я вижу 2 огромных плюса:
    - convention over configuration, но при этом большая гибкость. Видел примеры когда при переходе с другого тестового фреймворка просто настраивали convention чтобы старые тесты работали.
    - отсутствие в комплекте Assert библиотеки. Т.е. нету зависимости от "настроения авторов библиотеки", как в примере Сергея, а можно сразу использовать что то приличное типа FluentAssertion или Should

    ОтветитьУдалить
  5. А чего бы не наваять собственных ассертов с текстовыми сообщениями и прочим; которые, в конце концов, вызывают Assert.True?

    ОтветитьУдалить
    Ответы
    1. Да так и сделали. Но получается путаница: какими методами пользоваться Xunit.Assert.Equal или XAssert.Equals? Кто-то использует одни, кто-то другие. Получается нужно постоянно переключать сознание при чтении или изменении кода.

      Удалить
  6. Распространенное мнение, про избыточность сообщений в утверждениях. Мнение из книги The Art of Unit Testing более развернутое и менее догматичное:

    Avoid writing your own custom assert messages. Please. This section is for those who find they absolutely have to write a custom assert message, because the test really needs it, and you can’t find a way to make the test clearer without it. Writing a good assert message is much like writing a good exception message. It’s easy to get it wrong without realizing it, and it makes a world of difference (and time) to the people who have to read it.

    There are several key points to remember when writing a message for an assert clause:
    ■ Don’t repeat what the built-in test framework outputs to the console.
    ■ Don’t repeat what the test name explains.
    ■ If you don’t have anything good to say, don’t say anything.
    ■ Write what should have happened or what failed to happen, and possibly mention when it should have happened.

    А авторы XUnit, если уж решили своим фреймворком учить людей программировать, то выпиливали бы уже assert message из всех методов, а то непоследовательно выходит. И потом, у них висела раньше на страничке утверждение, мол NUnit так плохо сделан, что для продолжения развития надо бы его с нуля переписать, что и делает XUnit. По факту же новых возможностей не видно, зато переименовали атрибуты и сделали несовместимый Assert.

    ОтветитьУдалить
    Ответы
    1. Арсений, ценнейший комментарий. Очень толковое мнение Роя, развернутое и прагматичное.

      Удалить
  7. У нас NUnit.
    Пока ещё версию 2.6. В планах 3.0 чтобы запускать параллельно тесты.
    Ничего плохого сказать не могу.

    ОтветитьУдалить
  8. NUnit везде 2.6.4, в планах 3.0, но там много переделано.

    ОтветитьУдалить
  9. NUnit везде 2.6.4, в планах 3.0, но там много переделано.

    ОтветитьУдалить
  10. NUnit 3.0, как-то плохо работает с тестовым окном студии, у меня не получилось сделать так, что бы тесты можно было запускать через студию.

    ОтветитьУдалить
    Ответы
    1. Для этого нужно поставить тест адаптер. Либо глобальный на всю студию, либо в виде нюгет-пакета.

      Удалить
    2. А через Resharper работает нормально ?

      Удалить
    3. Начиная с 2016.1, кажется поддерживается из коробки. До этого - через расширение.

      Удалить
    4. По NUnit.
      Через R# работает более менее, если тестов не очень большое количество. На VS 2015 R# тест раннер стабильно зависает через некоторое время после старта 26 000 тестов (вернее одного теста с тест-кейс сорсом, который возвращает 26 000 вариантов). Поэтому, когда нужно такое кол-во тестов гонять перехожу на 2.6.4 и стандартный NUnit runner. Почему не 3.0 и стандартный runner? Потому что стандартный раннер под 3.0 только неудобная консоль без хорошей обратной связи. Которая еще по ощущениям работает заметно медленнее, чем тот же раннер R#, то есть проще на 2.6.4 в один поток прогнать, чем в несколько через console runner. Под 3.0 есть отдельный гуевый проект, но он настолько ужасен, что можно сказать, что его нет. Какой-то давно не обновлявшийся прототип.

      Также у NUnit уже вышла 3.2 версия, но ее никто не поддерживает, кроме, понятное дело, консольного раннера.

      Удалить
  11. А кто-нибудь пробовал Smart Unit Tests/IntelliTest от Микрософт? Который в Ultimate и может сам генерировать тестовы классы?
    Мне всегда нехватало возможности (в VS2010 была) кликнуть на методе и сгенерить шаблон теста из него.

    ОтветитьУдалить
  12. На нынешней работе видимо давно в традиции заведено xUnit. До этого я пользовался NUnit. И вот с чем абсолютно согласен - отсутствие внятной документации. Приходится по крохам собирать с разных форумов.

    ОтветитьУдалить
  13. По поводу комментариев, автор очень активно отвечает в slack чате.

    ОтветитьУдалить