Насколько глубоки ваши юнит-тесты?
Что я обнаружил в TDD, так это то, что для настройки ваших тестов требуется время и, естественно, лень, я всегда хочу написать как можно меньше кода. Первое, что я делаю, это проверяю, что мой конструктор установил все свойства, но это перебор?
Мой вопрос: на каком уровне детализации вы пишете юнит-тесты?
... и есть ли случай тестирования слишком много?
17 ответов
Мне платят за работающий код, а не за тесты, поэтому моя философия заключается в том, чтобы как можно меньше тестировать, чтобы достичь заданного уровня достоверности (я подозреваю, что этот уровень достоверности высок по сравнению с отраслевыми стандартами, но это может быть просто гордыней), Если я обычно не совершаю какую-то ошибку (например, устанавливаю неправильные переменные в конструкторе), я не проверяю это. Я обычно разбираюсь в ошибках тестирования, поэтому я особенно осторожен, когда у меня логика со сложными условными выражениями. При программировании в команде я изменяю свою стратегию, чтобы тщательно тестировать код, который мы, как правило, ошибаемся.
У разных людей будут разные стратегии тестирования, основанные на этой философии, но это кажется мне разумным, учитывая незрелое состояние понимания того, как тесты могут наилучшим образом вписаться во внутренний цикл кодирования. Через десять или двадцать лет у нас, вероятно, будет более универсальная теория о том, какие тесты писать, какие тесты не писать и как определить разницу. Тем временем экспериментирование кажется в порядке.
Напишите модульные тесты для вещей, которые вы ожидаете сломать, и для крайних случаев. После этого следует добавлять тестовые примеры по мере поступления отчетов об ошибках - перед написанием исправления ошибки. Разработчик может быть уверен, что:
- Ошибка исправлена;
- Ошибка не появится снова.
Согласно приложенному комментарию - я полагаю, что такой подход к написанию модульных тестов может вызвать проблемы, если со временем будет обнаружено много ошибок в данном классе. Это, вероятно, где усмотрение полезно: добавление модульных тестов только для ошибок, которые могут повториться, или когда их повторное появление может вызвать серьезные проблемы. Я обнаружил, что мера интеграционного тестирования в модульных тестах может быть полезна в этих сценариях - тестирование путей кода с более высоким кодом может охватывать пути кода ниже.
Все должно быть сделано как можно проще, но не проще. - А. Эйнштейн
Одним из самых неправильно понятых вещей о TDD является первое слово в нем. Тестовое задание. Вот почему BDD пришли вместе. Потому что люди действительно не понимали, что первый D был важным, а именно Driven. Все мы, как правило, много думаем о тестировании, и чуть-чуть о вождении дизайна. И я предполагаю, что это неопределенный ответ на ваш вопрос, но вам, вероятно, следует подумать о том, как управлять вашим кодом, а не тем, что вы на самом деле тестируете; Это то, с чем может помочь инструмент Покрытия. Дизайн - более серьезный и проблемный вопрос.
Для тех, кто предлагает тестировать "все": осознайте, что "полностью тестирующий" метод, подобный int square(int x)
требуется около 4 миллиардов тестовых случаев на общих языках и типичных средах.
На самом деле, это даже хуже, чем это: метод void setX(int newX)
также обязан не изменять значения любых других членов, кроме x
- ты это тестируешь? obj.y
, obj.z
и т. д. все остаются неизменными после звонка obj.setX(42);
?
Практично проверить подмножество "всего". Как только вы принимаете это, становится более приемлемым рассмотреть возможность не проверять невероятно простое поведение. У каждого программиста есть распределение вероятностей мест ошибок; разумный подход заключается в том, чтобы сосредоточить свою энергию на тестировании регионов, где вы оцениваете вероятность ошибки как высокую.
Классический ответ - "проверь все, что может сломаться". Я понимаю, что это означает, что тестирование сеттеров и геттеров, которые ничего не делают, кроме set или get, вероятно, слишком сложное тестирование, не нужно тратить время. Если ваша IDE не напишет их для вас, вы тоже можете.
Если ваш конструктор, не задавший свойства, может позже привести к ошибкам, то тестирование их установки не является чрезмерным.
Часть проблемы с пропуском простых тестов сейчас заключается в том, что в будущем рефакторинг может сделать это простое свойство очень сложным с большим количеством логики. Я думаю, что лучшая идея заключается в том, что вы можете использовать тесты для проверки требований к модулю. Если при прохождении X вы должны вернуть Y, то это то, что вы хотите проверить. Затем, когда вы позже измените код, вы можете проверить, что X дает вам Y, и вы можете добавить тест для A, который дает вам B, когда это требование будет добавлено позже.
Я обнаружил, что время, которое я трачу на начальные тестовые разработки, окупается в первом или втором исправлении ошибки. Возможность подобрать код, который вы не просматривали в течение 3 месяцев, и быть уверенным, что ваше исправление охватывает все случаи, и что "вероятно" ничего не сломало, чрезвычайно ценно. Вы также обнаружите, что модульные тесты помогут сортировать ошибки намного дальше трассировки стека и т. Д. Просмотр того, как отдельные части приложения работают и дают сбой, дает огромное представление о том, почему они работают или дают сбой в целом.
Я пишу тесты, чтобы покрыть предположения классов, которые я напишу. Тесты обеспечивают соблюдение требований. По сути, если x никогда не может быть 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 также должен быть способ проверки того, что другие свойства объекта не изменились. Хм.. назад к чертежной доске.
Таким образом, чем больше я пишу тесты, тем меньше беспокоюсь об уровне детализации тестирования. Оглядываясь назад, кажется, что я делаю простейшую вещь, чтобы достичь своей цели проверки поведения. Это означает, что я генерирую уровень уверенности в том, что мой код выполняет то, что я прошу, однако это не считается абсолютной гарантией того, что мой код не содержит ошибок. Я чувствую, что правильным балансом является тестирование стандартного поведения и, возможно, крайний случай или два, а затем переход к следующей части моего проекта.
Я принимаю, что это не будет охватывать все ошибки, и использую другие традиционные методы тестирования для их выявления.
Я делаю модульное тестирование, чтобы достичь максимально возможного покрытия. Если мне не удается получить доступ к некоторому коду, я выполняю рефакторинг до тех пор, пока охват не станет максимально полным
После завершения слепого теста на написание я обычно пишу один тест, воспроизводящий каждую ошибку
Я привык разделять тестирование кода и интеграционное тестирование. Во время интеграционного тестирования (которое также является модульным тестом, но для групп компонентов, поэтому не совсем то, для чего предназначено модульное тестирование), я проверю правильность выполнения требований.
Протестируйте исходный код, который вас беспокоит.
Бесполезно тестировать части кода, в которых вы очень уверены, если вы не допускаете в них ошибок.
Проверяйте исправления ошибок, чтобы исправить ошибки в первый и последний раз.
Протестируйте, чтобы получить уверенность в неясных частях кода, чтобы создавать знания.
Тестируйте перед интенсивным и средним рефакторингом, чтобы не нарушать существующие функции.
Я не занимаюсь модульными тестами простых методов установки / получения, которые не имеют побочных эффектов. Но я делаю юнит-тестирование любым другим публичным методом. Я пытаюсь создать тесты для всех граничных условий в моих algorthims и проверяю охват моих модульных тестов.
Это много работы, но я думаю, что это того стоит. Я бы лучше написал код (даже тестирующий код), чем пошагово проходил код в отладчике. Я считаю, что цикл code-build-deploy-debug очень трудоемкий, и чем более исчерпывающими являются юнит-тесты, которые я включил в свою сборку, тем меньше времени я трачу на прохождение этого цикла code-build-deploy-debug.
Вы не сказали, для чего вы программируете архитектуру. Но для Java я использую Maven 2, JUnit, DbUnit, Cobertura и EasyMock.
Чем больше я читаю об этом, тем больше я думаю, что некоторые модульные тесты похожи на некоторые шаблоны: запах недостаточных языков.
Когда вам нужно проверить, действительно ли ваш тривиальный получатель возвращает правильное значение, это потому, что вы можете смешивать имя получателя и имя переменной-члена. Введите 'attr_reader: name' для ruby, и это больше не может произойти. Просто не возможно в Java.
Если ваш геттер становится нетривиальным, вы все равно можете добавить тест для него.
Как правило, я начинаю с малого, с входов и выходов, которые, я знаю, должны работать. Затем, исправляя ошибки, я добавляю больше тестов, чтобы убедиться, что все исправленные мной вещи протестированы. Это органично, и хорошо работает для меня.
Вы можете проверить слишком много? Вероятно, но, вероятно, лучше ошибиться с осторожностью в целом, хотя это будет зависеть от того, насколько критично ваше приложение.
Я думаю, что вы должны проверить все в своем "ядре" своей бизнес-логики. Getter и Setter, потому что они могут принимать отрицательное или нулевое значение, которое вы, возможно, не хотите принимать. Если у вас есть время (всегда зависит от вашего начальника), хорошо бы протестировать другую бизнес-логику и все контроллеры, которые вызывают этот объект (вы медленно переходите от модульного теста к интеграционному тесту).
Этот ответ больше для того, чтобы выяснить, сколько модульных тестов нужно использовать для данного метода, который, как вы знаете, вы хотите использовать для модульного тестирования из-за его важности / важности. Используя метод проверки базового пути McCabe, вы могли бы сделать следующее, чтобы количественно обеспечить лучшую достоверность покрытия кода, чем простое "покрытие заявления" или "покрытие ветви":
- Определите значение Cyclomatic Complexity вашего метода, который вы хотите использовать для модульного тестирования (например, Visual Studio 2010 Ultimate может рассчитать его для вас с помощью инструментов статического анализа; в противном случае вы можете вычислить его вручную с помощью метода flowgraph - http://users.csc.calpoly.edu/~jdalbey/206/Lectures/BasisPathTutorial/index.html)
- Перечислите базовый набор независимых путей, которые проходят через ваш метод - см. Ссылку выше для примера потоковой диаграммы
- Подготовьте модульные тесты для каждого независимого базового пути, определенного на шаге 2