Насколько глубоки ваши модульные тесты?


88

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

Мой вопрос в том, на каком уровне детализации вы пишете модульные тесты?

..и не слишком ли много тестов?

Ответы:


221

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

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


40
Мир не думает, что Кент Бек сказал бы это! Легионы разработчиков добросовестно добиваются 100% покрытия, потому что они думают, что именно так поступил бы Кент Бек! Я говорил многим, что вы сказали в своей книге по XP, что не всегда строго придерживаетесь Test First. Но я тоже удивлен.
Чарли Флауэрс,

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

2
Меня не интересует освещение. Меня очень интересует, как часто мистер Бек фиксирует код, который не был написан в ответ на неудачный тест.
sheldonh

1
@RicardoRodrigues, вы не можете писать тесты для покрытия кода, который другие люди напишут позже. Это их ответственность.
Киеф

2
Я не об этом писал, прочтите внимательно; Я писал, что если вы пишете тесты, чтобы покрыть только часть ВАШЕГО собственного кода, оставляя непокрытые части, где «вы знаете, что не делаете ошибок», и эти части меняются и не имеют надлежащих тестов, у вас есть проблема прямо здесь, и это совсем не TDD.
Рикардо Родригес,

20

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

  1. Ошибка исправлена;
  2. Ошибка больше не появится.

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


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

@JohnNolan: Так ли важна читабельность теста? IMHO это не так, по крайней мере, для этих регрессионных тестов, специфичных для ошибок. Если вы часто переписываете тесты, возможно, вы тестируете на слишком низком уровне - в идеале ваши интерфейсы должны оставаться относительно стабильными, даже если ваши реализации изменятся, и вы должны тестировать на уровне интерфейса (хотя я понимаю, что реальный мир часто не работает) Мне нравится это ...: - /) Если ваши интерфейсы сильно изменятся, я бы предпочел отказаться от большинства или всех этих тестов, специфичных для ошибок, а не перезаписывать их.
j_random_hacker

@j_random_hacker Да, конечно, читабельность важна. Тесты - это форма документации, и они так же важны, как и производственный код. Я согласен с тем, что отмена тестов для серьезных изменений - это хорошо (tm) и что тесты следует проводить на уровне интерфейса.
Johnno Nolan,

19

Все должно быть сделано как можно проще, но не проще. - А. Эйнштейн

Одна из самых непонятых вещей о TDD - это первое слово в ней. Контрольная работа. Вот почему появился BDD. Потому что люди не понимали, что первое D было самым важным, а именно Driven. Мы все склонны немного слишком много думать о тестировании и немного мало о разработке дизайна. И я предполагаю, что это расплывчатый ответ на ваш вопрос, но вам, вероятно, следует подумать о том, как управлять своим кодом, а не о том, что вы на самом деле тестируете; в этом вам может помочь инструмент покрытия. Дизайн - гораздо более серьезная и проблемная проблема.


Да, это расплывчато ... Означает ли это, что поскольку конструктор не является частью поведения, мы не должны его тестировать. Но я должен тестировать MyClass.DoSomething ()?
Джонно Нолан,

Что ж, зависит от: P ... тест конструкции часто является хорошим началом при попытке протестировать устаревший код. Но я бы, вероятно (в большинстве случаев) оставил строительный тест, когда начинал проектировать что-то с нуля.
kitofr 01

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

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

15

Тем, кто предлагает тестировать «все»: осознайте, что «полное тестирование» такого метода int square(int x)требует около 4 миллиардов тестовых случаев на общих языках и типичных средах.

На самом деле, это даже хуже , чем: метод void setX(int newX)также обязан не изменять значения любых других членов , кроме x- вы тестирования , что obj.y, obj.zи т.д. все остаются неизменными после вызова obj.setX(42);?

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


9

Классический ответ - «проверьте все, что может сломаться». Я интерпретирую это как означающее, что тестирующие сеттеры и геттеры, которые ничего не делают, кроме set или get, вероятно, слишком много тестирования, не нужно тратить время. Если ваша IDE не напишет их за вас, вы тоже можете это сделать.

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


ага, и это привязка для класса с множеством свойств и множеством конструкторов.
Джонно Нолан,

Чем тривиальнее проблема (например, забыть инициализировать член в ноль), тем больше времени потребуется на ее отладку.
Лев

5

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

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


5

Отчасти проблема с пропуском простых тестов сейчас в будущем, рефакторинг может сделать это простое свойство очень сложным с большим количеством логики. Я думаю, что лучше всего использовать тесты для проверки требований к модулю. Если при прохождении X вы должны вернуть Y, то это то, что вы хотите проверить. Затем, когда вы измените код позже, вы можете убедиться, что X дает вам Y, и вы можете добавить тест для A, который дает вам B, когда это требование будет добавлено позже.

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


4

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

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

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

Итак, нет - я бы сказал, что нет такой вещи, как «слишком много» тестирования в общем смысле, только для отдельных лиц.


3

Разработка через тестирование означает, что вы прекращаете кодирование, когда все тесты пройдены.

Если у вас нет теста на свойство, зачем вам его реализовывать? Если вы не тестируете / не определяете ожидаемое поведение в случае «незаконного» присвоения, что должно делать свойство?

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

Чтобы упростить это тестирование, я создал простой NUnit, TestFixtureкоторый предоставляет точки расширения для установки / получения значения, принимает списки допустимых и недопустимых значений и имеет один тест, чтобы проверить, правильно ли работает свойство. Тестирование отдельного свойства может выглядеть так:

[TestFixture]
public class Test_MyObject_SomeProperty : PropertyTest<int>
{

    private MyObject obj = null;

    public override void SetUp() { obj = new MyObject(); }
    public override void TearDown() { obj = null; }

    public override int Get() { return obj.SomeProperty; }
    public override Set(int value) { obj.SomeProperty = value; }

    public override IEnumerable<int> SomeValidValues() { return new List() { 1,3,5,7 }; }
    public override IEnumerable<int> SomeInvalidValues() { return new List() { 2,4,6 }; }

}

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

PS: Вероятно, PropertyTest также должен иметь способ проверки того, что другие свойства объекта не изменились. Хм .. вернемся к чертежной доске.


Сходил на презентацию на mbUnit. Это здорово выглядит.
Джонно Нолан,

Но Дэвид, позвольте мне спросить вас: были ли вы удивлены ответом Кента Бека выше? Его ответ заставляет вас задуматься, стоит ли пересмотреть свой подход? Конечно, не потому, что у кого-то есть «ответы свыше». Но Кент считается одним из основных сторонников тестирования в первую очередь. Пенни за твои мысли!
Чарли Флауэрс,

@Charly: Ответ Кента очень прагматичный. Я «просто» работаю над проектом, в котором я буду интегрировать код из различных источников, и я хотел бы обеспечить очень высокий уровень уверенности.
Дэвид Шмитт

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

1

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

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

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


1

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

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


0

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

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


0

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


0

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

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

Вы не сказали, зачем вы тоже пишете архитектуру. Но для Java я использую Maven 2 , JUnit , DbUnit , Cobertura и EasyMock .


Я не сказал, что это за вопрос, не зависящий от языка.
Джонно Нолан,

Модульное тестирование в TDD не только охватывает вас во время написания кода, но также защищает от человека, который наследует ваш код, а затем считает, что имеет смысл форматировать значение внутри получателя!
Paxic,

0

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

Когда вам нужно проверить, действительно ли ваш тривиальный получатель возвращает правильное значение, это потому, что вы можете смешивать имя получателя и имя переменной-члена. Введите attr_reader: name для рубина, и этого больше не может быть. Просто невозможно в java.

Если ваш геттер когда-либо станет нетривиальным, вы все равно можете добавить для него тест.


Я согласен, что тестирование геттера тривиально. Однако я могу быть достаточно глупым, чтобы забыть установить его в конструкторе. Поэтому нужен тест. Мои мысли изменились с тех пор, как я задал вопрос. См. Мой ответ stackoverflow.com/questions/153234/how-deep-are-your-unit-tests/…
Johnno Nolan

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

@Damien: Может быть, юнит-тесты и контракты - это действительно одно и то же? Я имею в виду, что язык, который «поддерживает» контракты, в основном просто упрощает написание фрагментов кода - тестов, которые (необязательно) выполняются до и после других фрагментов кода, правильно? Если его грамматика достаточно проста, язык, который изначально не поддерживает контракты, можно легко расширить для их поддержки, написав препроцессор, верно? Или есть вещи, которые один подход (контракты или модульные тесты) может сделать, а другой просто не может?
j_random_hacker 09

0

Проверьте исходный код, который вас беспокоит.

Бесполезно тестировать части кода, в которых вы очень уверены, если вы не делаете в них ошибок.

Тестируйте исправления, так что это будет первый и последний раз, когда вы исправляете ошибку.

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

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


0

Этот ответ больше предназначен для того, чтобы выяснить, сколько модульных тестов использовать для данного метода, который, как вы знаете, вы хотите выполнить, из-за его критичности / важности. Используя технику Basis Path Testing от McCabe, вы можете сделать следующее, чтобы количественно получить более надежное покрытие кода, чем простое «покрытие операторов» или «покрытие ветвей»:

  1. Определите значение Cyclomatic Complexity для вашего метода, который вы хотите протестировать (например, Visual Studio 2010 Ultimate может рассчитать это за вас с помощью инструментов статического анализа; в противном случае вы можете рассчитать его вручную с помощью метода потокового графа - http://users.csc. calpoly.edu/~jdalbey/206/Lectures/BasisPathTutorial/index.html )
  2. Перечислите базовый набор независимых путей, которые проходят через ваш метод - см. Ссылку выше для примера потокового графа.
  3. Подготовьте модульные тесты для каждого независимого базового пути, определенного на шаге 2

Вы собираетесь делать это для каждого метода? Шутки в сторону?
Кристофер Джонсон,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.