Почему юнит-тесты должны тестировать только одно?

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

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

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

Пример: класс списка. Почему я должен делать отдельные тесты для добавления и удаления? Один тест, который сначала добавляет, а затем удаляет звуки проще.

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

Dave DuPlantis 25.10.2008 00:37

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

tchen 01.09.2010 13:02

Ответ на вопрос «Считаете ли вы, что модульные тесты - это бомба?» обычно сводится к вопросу «Насколько хорошо вы разбираетесь в макетах и ​​архитектуре кода?». Если вы не можете разбить свой код на отдельные блоки для тестирования (имитируйте входы и выходы и запускайте только тестируемый код), то модульные тесты просто не подходят. Вы обнаружите, что снова и снова пишете одни и те же сетапы / разборки, и на их выполнение уйдет целая вечность.

TheGrimmScientist 07.08.2017 05:28
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
59
3
11 496
17
Перейти к ответу Данный вопрос помечен как решенный

Ответы 17

Тестирование только одного элемента позволит выделить его и доказать, работает оно или нет. В этом заключается идея модульного тестирования. Нет ничего плохого в тестах, которые проверяют более одного объекта, но это обычно называется интеграционным тестированием. У них обоих есть достоинства, в зависимости от контекста.

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

Обновление: я прочитал эту статью и связанные статьи, и я должен сказать, я потрясен: https://techbeacon.com/app-dev-testing/no-1-unit-testing-best-practice-stop-doing-it

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

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

iny 24.10.2008 23:55

«Модульное» тестирование, по определению, проверяет модуль вашей программы (то есть по отдельности) за раз.

wprl 24.10.2008 23:57

Безусловно, вы можете сделать это таким образом, если это сработает для вас. Мне нелегко подчиняться методологиям. Я просто делаю то, что работает в контексте.

MrBoJangles 25.10.2008 00:05

@iny - Конечно, но если для выполнения тестового прогона требуется 30 минут, вам может потребоваться более подробный отчет о тестировании и одновременно исправить кучу

Newtopian 25.10.2008 00:22

@Newtopian - Выполнить только неудавшийся тест довольно просто.

iny 25.10.2008 00:24

Когда вы пишете узконаправленные модульные тесты, вы можете быстрее находить и исправлять дефекты в коде. Также легче измерить свой прогресс; Если вы тестируете 50 функций, более целесообразно показать, что 30 тестов за 50 пройдены, чем показать этот 1 тест за 5 прохождений.

Dave DuPlantis 25.10.2008 00:36

Как показывает практика, это звучит довольно разумно. Но на это утверждение, как и на большинство других, можно законно ответить, сказав: «Это зависит от обстоятельств».

MrBoJangles 25.10.2008 01:35

Люблю свой значок / фото! Вы настоящий вундеркинд.

DOK 25.10.2008 19:07

Я думаю, что буду педантичен и укажу, что единица - это совокупность или контейнер «вещей». Двигайтесь как единое целое, как единое целое и так далее.

Chris S 27.02.2009 15:57

Я согласен с этим ответом. Достаточно интересно, что если вы пишете небольшие тесты, которые проверяют только одну вещь, вы в конечном итоге тяготеете к дизайну, который легче тестировать отдельные вещи ... который, как правило, требует меньшего состояния настройки, что в конечном итоге намного быстрее, чем большие интеграционные тесты, и предоставляет гораздо больше больше уверенности. Я думаю, что реалистичной целью должно быть выполнение 80 000 тестов менее чем за 10 минут. То есть подавляющее большинство ваших тестов должно выполняться менее чем за 10 мс каждый с меньшим количеством (но важных) интеграционных тестов. Ни один не работает дольше 10 секунд. Проверка рациона временем.

justin.m.chase 18.10.2012 02:38

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

[Редактировать:] Хорошо, скажем, это образец метода тестирования:

[TestMethod]
public void TestSomething() {
  // Test condition A
  // Test condition B
  // Test condition C
  // Test condition D
}

Если ваш тест на условие A не прошел, то B, C и D также окажутся неудачными и не принесут вам никакой пользы. Что, если бы изменение вашего кода также привело бы к сбою C? Если бы вы разделили их на 4 отдельных теста, вы бы это знали.

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

iny 24.10.2008 23:59

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

swilliams 25.10.2008 00:04

Тем не менее, это хорошее обсуждение, и мне нравится, что вы отстаиваете свое мнение, даже если я думаю, что вы ошибаетесь :)

swilliams 25.10.2008 00:04

Смотрите дополнение в вопросе.

iny 25.10.2008 00:26

Если вы тестируете более одного объекта, и первое, что вы тестируете, терпит неудачу, вы не узнаете, пройдут ли последующие объекты, которые вы тестируете, или нет. Это легче исправить, когда вы знаете все, что выйдет из строя.

Хааа ... юнит-тесты.

Если зайти слишком далеко с любыми «директивами», он быстро станет непригодным для использования.

Единичный модульный тест - это такая же хорошая практика, как и одиночный метод, выполняющий единственную задачу. Но IMHO, это не означает, что один тест может содержать только один оператор assert.

Является

@Test
public void checkNullInputFirstArgument(){...}
@Test
public void checkNullInputSecondArgument(){...}
@Test
public void checkOverInputFirstArgument(){...}
...

лучше чем

@Test
public void testLimitConditions(){...}

На мой взгляд, это вопрос вкуса, а не хорошей практики. Я лично предпочитаю последнее.

Но

@Test
public void doesWork(){...}

на самом деле "директива" хочет, чтобы вы избегали любой ценой и что быстрее всего истощает мое рассудок.

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

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

Мои 2 цента.

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

MrBoJangles 25.10.2008 00:12

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

TheGrimmScientist 07.08.2017 05:24
Ответ принят как подходящий

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

Иногда тесты требуют определенной настройки. Иногда для их настройки может потребоваться определенное количество время (в реальном мире). Часто вы можете протестировать два действия за один раз.

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

Против: если действие либо завершится неудачно, вы получите тот же результат: тот же тест не удастся. У вас будет меньше информации о том, в чем проблема, чем если бы вы выполняли только одно действие в каждом из двух тестов.

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

Немного другой «минус» здесь состоит в том, что он прерывает цикл «написать новый тест, пройти, рефакторинг». Я рассматриваю это как цикл идеальный, но тот, который не всегда отражает реальность. Иногда просто более прагматично добавить дополнительное действие и проверку (или, возможно, просто еще одну проверку к существующему действию) в текущем тесте, чем создавать новый.

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

David Arno 25.10.2008 00:05

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

Dave DuPlantis 25.10.2008 00:39

Слова что-то значат: модульный тест должен тестировать одну единицу программы. Один метод, одна функция. Интеграционные и функциональные тесты (которые можно автоматизировать!) Проверяют блоки большего размера. Я также проголосовал против, так как у спрашивающего, казалось, уже был ответ, и проигнорировал ответ, набрав больше голосов.

Terry G Lorber 05.02.2009 01:10

@Terry: Теоретически это звучит прекрасно, но, на мой взгляд, на практике это не работает в 100% случаев. Если в случаях немного вы получите более простой и меньший код, протестировав два действия в одном тестовом примере, где преимущество практичный в том, что этого не происходит?

Jon Skeet 05.02.2009 01:42

@Jon: На практике мне легче тестировать небольшие фрагменты, YMMV. Ничего не работает на 100%, поэтому выбирайте с умом. Я бы добавил это в качестве довода против того, чтобы не выполнять надлежащие модульные тесты: требования написания кода для модульного тестирования могут принести пользу дизайну программного обеспечения (абстракция, инкапсуляция, короткие методы и т. д.)

Terry G Lorber 10.02.2009 06:48

@Terry: Мне нравится тестировать небольшие фрагменты где я могу, но я бы предпочел тестировать большие фрагменты, чем не тестировать вообще, и я бы предпочел иметь большие фрагменты с кодом, в котором я уверен, чем без кода вообще, потому что я могу '' Я не хочу вдаваться в подробности в небольших тестах. Цельтесь высоко, но будьте практичны.

Jon Skeet 10.02.2009 09:30

@Jon: Пример списка в Q. описывает множество утверждений в рамках одного метода тестирования. Для меня это все еще звучит как модульный тест, ничего плохого в повторном использовании настройки. Согласен, лучше большой тест, например, CPPUNIT_ASSERT (true == irs.paidTaxesLastFiveYears ()), а затем без теста. Семантика утверждения против теста?

Terry G Lorber 10.02.2009 16:27

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

Jon Skeet 10.02.2009 16:40

Одна проблема с тестированием большими порциями заключается в том, что тогда тесты предоставляют меньшую ценность для документации: короткое имя в большом тесте не может описать полное намерение автора, что, в свою очередь, требует от читателя реконструировать намерение автора из тестового кода. и реализация. См. Мой ответ и статью, на которую есть ссылка.

Esko Luontola 28.02.2010 02:35

@Esko: Я не предлагаю тесты громадный - просто не «одно утверждение на тест». Можно найти золотую середину.

Jon Skeet 28.02.2010 10:14

Хорошо, что вы не предлагаете огромных тестов, но показ некоторого кода прояснит, что вы имеете в виду - «маленький» для одного человека может быть «большим» для другого. «Одно утверждение на тест» можно интерпретировать по-разному. Жесткая буквальная интерпретация «один оператор утверждения на тест» не всегда практична, потому что отдельные утверждения утверждения имеют очень низкий уровень, и иногда вы не можете проверить значение только одним утверждением, но интерпретация более высокого уровня «одна концепция на тест» важнее. См. «Чистый код», страницы 130–132.

Esko Luontola 28.02.2010 10:50

@Esko: Тип кода, в котором это действительно имеет значение, - это бизнес-код с сложной настройкой и т. д. Примеры, такие как коллекции, в конечном итоге предпочитают очень маленькие тесты по самой своей природе - только код настоящий действительно имеет значение, и это обычно конфиденциально. Я по-прежнему считаю, что есть много места для тестов различного размера - некоторые очень низкого уровня, а некоторые относительно высокого уровня (даже если они все еще не являются полными приемочными тестами).

Jon Skeet 28.02.2010 10:58

Что ж .. это касается того же вопроса, что каждый тест должен иметь только одно утверждение. Я считаю, что проблема в том, что каждый тест должен быть атомарным. Если у вас более одного утверждения, у вас больше нет атомарного теста, поскольку вы проверяете что-либо только до первого неудачного утверждения.

txwikinger 03.07.2010 00:08

@txwikinger: Возникает вопрос, почему вы считаете, что каждый тест должен быть атомарным. Я искренне верю, что тестирование должно быть прагматичным. Если использование более одного утверждения (для связанных вещей, конечно) в тесте делает этот тест более легким для чтения и более быстрым для написания, чем их разделение, я не вижу большого вреда. Обычно, если первое утверждение терпит неудачу, это указывает на проблему, которая, вероятно, в любом случае приведет к сбою второго утверждения. Не могу поверить, что при таком подходе я потерял значительное количество времени - скорее, я считаю, что сэкономил время и получил более понятные тесты.

Jon Skeet 03.07.2010 00:26

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

txwikinger 03.07.2010 00:53

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

Jon Skeet 03.07.2010 00:57

@Jon Skeet: Может быть, это одно из тех исключений из правил. Однако, возможно, ваш код нуждается в рефакторинге из-за того, что слишком много вещей являются частью одной логической операции. Такие вопросы всегда сложно обсуждать чисто абстрактно. IMHO имеет смысл попытаться разработать модульные тесты, не требующие таких сложных сборок, поскольку это создает риск того, что такие тесты труднее поддерживать, когда проект повторяется и все меняется, в частности, рефакторинг. Как указывалось ранее, руководящие принципы подходят не для каждой ситуации, и различные аспекты качества часто противоречат друг другу.

txwikinger 03.07.2010 01:18

Раньше меня это очень сильно обжигало. На что вы должны обратить внимание, это когда вы разделяете состояние настройки между тестами, потому что «медленно» воссоздавать его каждый раз (красный флаг), и один из тестов не очищает себя должным образом. Это легко исправить, и в итоге вы получите случайные неудачные тесты, поскольку, когда тесты проходят изолированно или в другом порядке, обнаружение ошибки может быть настоящим кошмаром. Однако тестировать только одну вещь не означает только одно утверждение. Используйте свое усмотрение и придерживайтесь ААА, если вы действуете после утверждения, то это красный флаг.

justin.m.chase 18.10.2012 02:31

В pytest вы можете запустить установку только один раз для группы тестов. Я думаю, что это просто вопрос технологии, а не повод делать тесты некрасивыми, смешивая разную логику.

uhbif19 26.04.2019 13:14

GLib, но, надеюсь, все еще полезный, ответ таков: unit = one. Если вы тестируете более одного объекта, значит, вы не тестируете модуль.

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

Например, у меня может быть метод, принимающий параметр. Одна из вещей, о которой я мог бы подумать в первую очередь, - что должно произойти, если параметр равен нулю? Он должен выбросить исключение ArgumentNull (я думаю). Поэтому я пишу тест, который проверяет, возникает ли это исключение, когда я передаю нулевой аргумент. Запустите тест. Хорошо, это выдает NotImplementedException. Я исправляю это, изменяя код, чтобы генерировать исключение ArgumentNull. Запустите мой тест, он прошел. Тогда я думаю, что будет, если он слишком маленький или слишком большой? Ах, это два теста. Сначала я напишу слишком маленький случай.

Дело в том, что я не думаю о поведении метода сразу. Я создаю его постепенно (и логически), думая о том, что он должен делать, а затем реализую код и рефакторинг по мере того, как я иду, чтобы он выглядел красиво (элегантно). Вот почему тесты должны быть небольшими и целенаправленными, потому что, когда вы думаете о поведении, вы должны развивать его небольшими, понятными шагами.

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

MrBoJangles 25.10.2008 00:14

Я особо не думал об этом, но да. Тестирование только одной вещи (или мелочей) делает возможным TDD. Если бы ваши тесты были большими, TDD был бы ужасным способом написания программного обеспечения.

tvanfosson 25.10.2008 00:36

Меньшие по размеру модульные тесты проясняют, в чем проблема, когда они терпят неудачу.

Что касается вашего примера: если вы тестируете добавление и удаление в одном модульном тесте, как вы проверяете, был ли элемент когда-либо добавлен в ваш список? Вот почему вам нужно добавить и убедиться, что он был добавлен за один тест.

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

Между ними проще добавить assert.

iny 25.10.2008 07:44

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

Вот наглядный пример. Допустим, у вас есть класс стека с запросами:

  • getSize
  • пусто
  • getTop

и методы для изменения стека

  • push (объект)
  • поп ()

Теперь рассмотрим следующий тестовый пример (в этом примере я использую псевдокод Python).

class TestCase():
    def setup():
        self.stack = new Stack()
    def test():
        stack.push(1)
        stack.push(2)
        stack.pop()
        assert stack.top() == 1, "top() isn't showing correct object"
        assert stack.getSize() == 1, "getSize() call failed"

Из этого тестового примера вы можете определить, что что-то не так, но не изолировано ли это от реализаций push() или pop() или запросов, возвращающих значения: top() и getSize().

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

def test_size():
    assert stack.getSize() == 0
    assert stack.isEmpty()

def test_push():
    self.stack.push(1)
    assert stack.top() == 1, "top returns wrong object after push"
    assert stack.getSize() == 1, "getSize wrong after push"

def test_pop():
    stack.push(1)
    stack.pop()
    assert stack.getSize() == 0, "getSize wrong after push"

Что касается разработки через тестирование. Я лично пишу более крупные «функциональные тесты», которые заканчиваются сначала тестированием нескольких методов, а затем создают модульные тесты, когда я начинаю реализовывать отдельные части.

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

Я все еще использую три вызова методов в test_push, однако и top(), и getSize() - это запросы, которые проверяются отдельными тестовыми методами.

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

Во-первых, мне кажется, что вы тестируете три метода в test_push, а не один, и вам все равно нужно посмотреть, что assert не удалось выяснить, что не так. И эти два теста не проверяют такое поведение, как исходный комбинированный тест. Так почему бы не провести комбинированный тест с дополнительными утверждениями?

Sol 04.02.2009 16:37

См. Сообщение для расширенного объяснения.

Ryan 05.02.2009 00:35

Я поддерживаю идею о том, что модульные тесты должны проверять только одно. Я также немного отклоняюсь от этого. Сегодня у меня был тест, в котором дорогая установка, казалось, заставляла меня делать более одного утверждения за тест.

namespace Tests.Integration
{
  [TestFixture]
  public class FeeMessageTest
  {
    [Test]
    public void ShouldHaveCorrectValues
    {
      var fees = CallSlowRunningFeeService();
      Assert.AreEqual(6.50m, fees.ConvenienceFee);
      Assert.AreEqual(2.95m, fees.CreditCardFee);
      Assert.AreEqual(59.95m, fees.ChangeFee);
    }
  }
}

В то же время мне очень хотелось увидеть все мои утверждения, которые потерпели неудачу, а не только первое. Я ожидал, что все они проиграют, и мне нужно было знать, какие суммы я действительно получаю. Но стандартный [SetUp] с каждым разделенным тестом вызовет 3 вызова медленной службы. Внезапно я вспомнил статью, в которой говорилось, что в использовании «нетрадиционных» тестовых конструкций скрыта половина преимуществ модульного тестирования. (Я думаю, что это был пост Джереми Миллера, но сейчас не могу его найти.) Внезапно [TestFixtureSetUp] всплыл в памяти, и я понял, что могу сделать один вызов службы, но все же иметь отдельные выразительные методы тестирования.

namespace Tests.Integration
{
  [TestFixture]
  public class FeeMessageTest
  {
    Fees fees;
    [TestFixtureSetUp]
    public void FetchFeesMessageFromService()
    {
      fees = CallSlowRunningFeeService();
    }

    [Test]
    public void ShouldHaveCorrectConvenienceFee()
    {
      Assert.AreEqual(6.50m, fees.ConvenienceFee);
    }

    [Test]
    public void ShouldHaveCorrectCreditCardFee()
    {
      Assert.AreEqual(2.95m, fees.CreditCardFee);
    }

    [Test]
    public void ShouldHaveCorrectChangeFee()
    {
      Assert.AreEqual(59.95m, fees.ChangeFee);
    }
  }
}

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

Коллега также отметил, что это немного похоже на specunit.net Скотта Беллвэра: http://code.google.com/p/specunit-net/

Другой практический недостаток очень детализированного модульного тестирования заключается в том, что оно нарушает СУХОЙ принцип. Я работал над проектами, в которых правилом было то, что каждый общедоступный метод класса должен иметь модульный тест ([TestMethod]). Очевидно, это добавляло некоторые накладные расходы каждый раз, когда вы создавали общедоступный метод, но реальная проблема заключалась в том, что это добавляло некоторое «трение» к рефакторингу.

Это похоже на документацию уровня метода, это приятно иметь, но это еще одна вещь, которую нужно поддерживать, и это делает изменение сигнатуры или имени метода немного более громоздким и замедляет «рефакторинг зубной нити» (как описано в «Инструменты рефакторинга: соответствие назначению» Эмерсона Мерфи-Хилла и Эндрю П. Блэк. PDF, 1,3 МБ).

Как и в большинстве случаев в дизайне, существует компромисс, который не отражает фраза «тест должен проверять только одно».

Подумайте о постройке машины. Если бы вы применили свою теорию о простом тестировании больших вещей, то почему бы не провести тест, чтобы проехать на машине по пустыне. Это ломается. Хорошо, скажи мне, в чем проблема. Вы не можете. Это тест сценария.

Функциональная проверка может заключаться в запуске двигателя. Это не удается. Но это могло быть по ряду причин. Вы все еще не могли сказать мне, что именно вызвало проблему. Но мы приближаемся.

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

Кто-то упомянул об использовании трассировки стека. Забудь это. Это второе средство. Просматривать трассировку стека или использовать отладку сложно и может занять много времени. Особенно на больших системах и сложных ошибках.

Хорошие характеристики юнит-теста:

  • Быстро (миллисекунды)
  • Независимый. Это не зависит от других тестов и не зависит от них.
  • Прозрачный. Он не должен быть раздутым или содержать огромное количество настроек.

Когда тест не проходит, есть три варианта:

  1. Реализация не работает и требует исправления.
  2. Тест не работает, и его нужно исправить.
  3. Тест больше не нужен, и его следует удалить.

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

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

Вот как это может выглядеть (с GoSpec), когда каждый тест проверяет только одно:

func StackSpec(c gospec.Context) {
  stack := NewStack()

  c.Specify("An empty stack", func() {

    c.Specify("is empty", func() {
      c.Then(stack).Should.Be(stack.Empty())
    })
    c.Specify("After a push, the stack is no longer empty", func() {
      stack.Push("foo")
      c.Then(stack).ShouldNot.Be(stack.Empty())
    })
  })

  c.Specify("When objects have been pushed onto a stack", func() {
    stack.Push("one")
    stack.Push("two")

    c.Specify("the object pushed last is popped first", func() {
      x := stack.Pop()
      c.Then(x).Should.Equal("two")
    })
    c.Specify("the object pushed first is popped last", func() {
      stack.Pop()
      x := stack.Pop()
      c.Then(x).Should.Equal("one")
    })
    c.Specify("After popping all objects, the stack is empty", func() {
      stack.Pop()
      stack.Pop()
      c.Then(stack).Should.Be(stack.Empty())
    })
  })
}

Разница здесь в том, что у вас есть вложенные тесты. Три теста: «нажатый последним выталкивается первым», «отправленный первым выталкивается последним» и «после этого стек пуст», по сути, являются подтестами. Это довольно изящный способ решения задач, но он не поддерживается (скажем) JUnit и NUnit. (Мне не очень нравится фраза «давайте сделаем все как по-английски», но это другой вопрос.) Как бы вы выразили эти тесты в JUnit? Как 5 отдельных тестов или 2? (Каждое из двух может содержать несколько утверждений - необязательно с сообщениями.)

Jon Skeet 28.02.2010 11:01

В JUnit 4 я бы использовал простой пользовательский бегун, чтобы я мог использовать внутренние классы следующим образом: github.com/orfjackal/tdd-tetris-tutorial/blob/beyond/src/tes‌ t /… В JUnit 3 он работает не так хорошо, но это возможно так: github.com/orfjackal/tdd-tetris-tutorial/blob/… В фреймворке, в котором нет приспособлений (например, as gotest), я бы неохотно записывал всю ту же информацию в имя одного метода. Отсутствие приспособлений приводит к большому дублированию.

Esko Luontola 28.02.2010 19:51

Я не использовал ни NUnit, ни C#, но из nunit.org/index.php?p=quickStart&r=2.5.3 кажется, что NUnit изначально поддерживает этот стиль организации тестов. Просто поместите несколько тестовых устройств в одно и то же пространство имен, чтобы в одном файле / пространстве имен были все тестовые устройства, которые относятся к одному и тому же поведению.

Esko Luontola 28.02.2010 20:02

Лучше всего, конечно, если среда тестирования уже поддерживает предпочтительный стиль написания тестов. В Java я в основном использовал JDave, в спецификациях Scala, в Ruby RSpec и т. д. И если ничего подходящего не существует, реализовать его самостоятельно можно за неделю. Так было с Go: единственным фреймворком был gotest, но он был слишком ограничен, gospecify находился в разработке, но у его автора были другие цели проекта (без изоляции побочных эффектов), поэтому я создал GoSpec 1.0 менее чем за 50 часов.

Esko Luontola 28.02.2010 20:28

Если вы тестируете более одного объекта, это называется интеграционным тестом ... а не модульным тестом. Вы все равно будете запускать эти интеграционные тесты в той же среде тестирования, что и ваши модульные тесты.

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

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

The real question is why make a test or more for all methods as few tests that cover many methods is simpler.

Что ж, чтобы, когда какой-то тест не прошел, вы знали, какой метод не работает.

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

An example: A list class. Why should I make separate tests for addition and removal? A one test that first adds then removes sounds simpler.

Предположим, что метод добавления не работает и не добавляет, а метод удаления не работает и не удаляет. Ваш тест проверит, что список после добавления и удаления имеет тот же размер, что и изначально. Ваш тест будет успешным. Хотя оба ваших метода сломались бы.

Отказ от ответственности: на этот ответ сильно повлияла книга «Тестовые шаблоны xUnit».

Тестирование только одного предмета в каждом тесте - один из самых основных принципов, который дает следующие преимущества:

  • Локализация дефекта: Если тест не прошел, вы сразу узнаете, почему он не прошел (в идеале без дальнейшего устранения неполадок, если вы хорошо поработали с используемыми утверждениями).
  • Тест как спецификация: тесты служат не только в качестве подстраховки, но и могут быть легко использованы в качестве спецификации / документации. Например, разработчик должен иметь возможность читать модульные тесты отдельного компонента и понимать его API / контракт, без необходимости читать реализацию (используя преимущества инкапсуляции).
  • Невозможность TDD: TDD основан на наличии фрагментов функциональности небольшого размера и выполнении последовательных итераций (написание неудачного теста, написание кода, проверка успешности теста). Этот процесс сильно прерывается, если тест должен проверить несколько вещей.
  • Отсутствие побочных эффектов: В некоторой степени связано с первым, но когда тест проверяет несколько вещей, более вероятно, что он будет привязан и к другим тестам. Таким образом, для этих тестов может потребоваться общая тестовая оснастка, а это значит, что на один будет влиять другой. Итак, в конечном итоге у вас может быть сбой теста, но на самом деле другой тест вызвал сбой, например путем изменения данных приспособления.

Я вижу только одну причину, по которой вам может быть полезно иметь тест, который проверяет несколько вещей, но на самом деле это следует рассматривать как запах кода:

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

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

iny 24.03.2017 10:49

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

Dimos 24.03.2017 15:53

Другие вопросы по теме