Как метод модульного теста, который возвращает коллекцию, избегая при этом логики в тесте

14

Я тест-драйв метод, который заключается в создании коллекции объектов данных. Я хочу убедиться, что свойства объектов установлены правильно. Некоторые свойства будут установлены одинаково; другим будет присвоено значение, которое зависит от их положения в коллекции. Естественный способ сделать это, кажется, с петлей. Однако Рой Ошеров настоятельно рекомендует не использовать логику в модульных тестах ( Art of Unit Testing , 178). Он говорит:

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

Как правило, тесты должны представлять собой серию вызовов методов без потоков управления, даже try-catchбез вызовов assert.

Тем не менее, я не вижу ничего плохого в своем дизайне (как еще вы генерируете список объектов данных, некоторые значения которых зависят от того, где в последовательности они находятся? - не могу точно сгенерировать и протестировать их отдельно). Что-то не подходит для тестирования с моим дизайном? Или я слишком жестко предан учению Ошерова? Или есть какая-то секретная магия юнит-теста, о которой я не знаю, которая обходит эту проблему? (Я пишу в C # / VS2010 / NUnit, но, если возможно, ищу не зависящие от языка ответы.)

Kazark
источник
4
Я рекомендую не зацикливаться. Если ваш тест состоит в том, что для третьей вещи Bar имеет значение Frob, то напишите тест, чтобы конкретно проверить, что Bar третьей вещи - Frob. Это один тест сам по себе, иди прямо к нему, без петель. Если ваш тест состоит в том, что вы получаете коллекцию из 5 вещей, это также один тест. Это не значит, что у вас никогда не будет цикла (явного или иного), просто вам это не нужно часто . Кроме того, относитесь к книге Ошерова как к большему количеству руководств, чем к действующим правилам.
Энтони Пеграм
1
Наборы @AnthonyPegram неупорядочены - иногда Frob может быть третьим, иногда вторым. Вы не можете на это полагаться, делая цикл (или языковую функцию, подобную Python in) необходимым, если тест «Frob был успешно добавлен в существующую коллекцию».
Изката
1
@Izbata, его вопрос особенно упоминает, что заказ важен. Его слова: «для других будет установлено значение, которое зависит от их положения в коллекции». Существует множество типов коллекций в C # (язык, на который он ссылается), которые упорядочены по вставке. В этом отношении вы также можете положиться на порядок со списками в Python, языке, который вы упоминаете.
Энтони Пеграм
Также предположим, что вы тестируете метод Reset для коллекции. Вам нужно перебрать коллекцию и проверить каждый предмет. В зависимости от размера коллекции, не проверять ее в цикле смешно. Или, скажем, я тестирую что-то, что должно увеличивать каждый элемент в коллекции. Вы можете установить для всех элементов одно и то же значение, вызвать приращение, а затем проверить. Этот тест отстой. Вы должны установить для нескольких из них разные значения, увеличить приращение и убедиться, что все разные значения увеличиваются правильно. Проверка только одного случайного элемента в коллекции оставляет много шансов.
iheanyi
Я не собираюсь отвечать таким образом, потому что я получу тысячу голосов, но я часто просто toString()коллекционирую и сравниваю с тем, что должно быть. Просто и работает.
user949300

Ответы:

16

TL; DR:

  • Написать тест
  • Если тест делает слишком много, код может сделать слишком много.
  • Это не может быть модульный тест (но не плохой тест).

Первое, что нужно проверить - догма бесполезна. Я с удовольствием читаю «Путь Фестиваля», в котором легко и искренне говорится о некоторых проблемах с догмой.

Напишите тест, который нужно написать.

Если тест нужно написать каким-то образом, запишите его так. Попытка заставить тест в некоторой идеализированной структуре теста или не иметь вообще ничего не является хорошей вещью. Сегодняшнее тестирование лучше, чем «идеальное» тестирование в более поздний день.

Я также укажу на немного уродливого теста:

Когда код некрасив, тесты могут быть некрасивыми.

Вы не любите писать уродливые тесты, но уродливый код нуждается в тестировании больше всего.

Не позволяйте уродливому коду помешать вам писать тесты, но позвольте уродливому коду помешать вам писать больше.

Это может считаться трюизмом для тех, кто следил за ним долгое время ... и они просто укоренились в мышлении и написании тестов. Для людей, которые не были и пытаются достичь этой точки, напоминания могут быть полезны (я даже нахожу, что их перечитывание помогает мне не попасться в какую-то догму).


Учтите, что при написании некрасивого теста, если код, это может быть признаком того, что код слишком много пытается сделать. Если код, который вы тестируете, слишком сложен, чтобы его можно было правильно выполнить путем написания простого теста, вы можете рассмотреть возможность разбить код на более мелкие части, которые можно протестировать с помощью более простых тестов. Не следует писать модульный тест, который делает все (тогда это может быть не модульный тест). Подобно тому, как «объекты бога» плохи, «тесты бога» тоже плохи и должны служить указанием вернуться назад и снова посмотреть на код.

Вы должны быть в состоянии выполнить весь код с разумным охватом с помощью таких простых тестов. Тесты, которые выполняют больше сквозных тестов, которые имеют дело с более крупными вопросами («У меня есть этот объект, упорядоченный в xml, отправленный в веб-сервис, через правила, откат назад и демаршаллирование») - отличный тест, но он, безусловно, не не является модульным тестом (и попадает в сферу интеграционного тестирования - даже если у него есть поддельные сервисы, которые он вызывает, и пользовательские базы данных памяти для тестирования). Он все еще может использовать платформу XUnit для тестирования, но каркас тестирования не делает его модульным тестом.


источник
7

Я добавляю новый ответ, потому что моя точка зрения отличается от того, когда я писал оригинальный вопрос и ответ; не имеет смысла объединять их в одно целое.

Я сказал в оригинальном вопросе

Тем не менее, я не вижу ничего плохого в своем дизайне (как еще вы генерируете список объектов данных, некоторые значения которых зависят от того, где в последовательности они находятся? - не могу точно сгенерировать и протестировать их отдельно)

Вот где я ошибся. Проработав функциональное программирование за последний год, я теперь понимаю, что мне просто нужна была операция сбора с аккумулятором. Тогда я мог бы написать свою функцию как чистую функцию, которая работала бы над одной вещью, и использовать некоторую стандартную библиотечную функцию, чтобы применить ее к коллекции.

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

Для более сложной логики используйте тесты на основе свойств . Когда у них действительно есть логика, она должна быть меньше и обратно по отношению к логике тестируемого кода, и каждый тест проверяет намного больше, чем модульный тест на основе регистра, что небольшой объем логики того стоит.

Прежде всего всегда опирайтесь на ваши типы . Получите самые сильные типы, которые вы можете, и используйте их в своих интересах. Это уменьшит количество тестов, которые вы должны написать в первую очередь.

Kazark
источник
4

Не пытайтесь проверять слишком много вещей одновременно. Каждое из свойств каждого объекта данных в коллекции слишком много для одного теста. Вместо этого я рекомендую:

  1. Если коллекция фиксированной длины, напишите модульный тест для проверки длины. Если это переменная длина, напишите несколько тестов для длин, которые будут характеризовать его поведение (например, 0, 1, 3, 10). В любом случае, не проверяйте свойства в этих тестах.
  2. Напишите модульный тест для проверки каждого из свойств. Если коллекция имеет фиксированную длину и короткую длину, просто укажите одно свойство каждого из элементов для каждого теста. Если это фиксированная длина, но длинная, выберите репрезентативную, но небольшую выборку элементов для утверждения по одному свойству каждый. Если это переменная длина, создайте относительно короткую, но репрезентативную коллекцию (то есть, может быть, три элемента) и подтвердите одно свойство каждого из них.

Делая это таким образом, вы делаете тесты достаточно маленькими, чтобы пропустить циклы не было болезненным. C # / Пример блока, данный тестируемый метод ICollection<Foo> generateFoos(uint numberOfFoos):

[Test]
void generate_zero_foos_returns_empty_list() { ... }
void generate_one_foo_returns_list_of_one_foo() { ... }
void generate_three_foos_returns_list_of_three_foos() { ... }
void generated_foos_have_sequential_ID()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("ID1", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID2", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID3", foos.Current.id);
}
void generated_foos_have_bar()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
}

Если вы привыкли к парадигме «тест плоского модуля» (без вложенных структур / логики), эти тесты кажутся довольно чистыми. Таким образом, логику избегают в тестах, идентифицируя исходную проблему как попытку проверить слишком много свойств одновременно, а не отсутствие циклов.

Kazark
источник
1
У Ошерова была бы твоя голова на блюде для того, чтобы иметь 3 утверждения. ;) Первый провал означает, что вы никогда не проверяете остальные. Обратите внимание, что вы действительно не избежали петли. Вы просто явно расширили его в исполняемую форму. Не жесткая критика, а просто предложение получить больше практики по изоляции ваших тестовых случаев до минимально возможного количества, чтобы дать себе более конкретную обратную связь, когда что-то не так, и в то же время продолжать проверять другие случаи, которые могут все еще пройти (или потерпеть неудачу, с свои конкретные отзывы).
Энтони Пеграм
3
@AnthonyPegram Я знаю о парадигме «один утверждать за тест». Я предпочитаю мантру «тестируй одно» (как отстаивал Боб Мартин против «одного утверждения за тест» в « Чистом коде» ). Примечание: модульные тесты, которые имеют «ожидают» и «утверждают», хороши (Google Tests). В остальном, почему бы вам не разделить ваши предложения на полный ответ с примерами? Я думаю, что я мог бы извлечь выгоду.
Казарк