Что делает хороший юнит-тест?

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

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

Языковые агностические предложения приветствуются.

18 ответов

Решение

Позвольте мне начать с подключения источников - прагматического модульного тестирования в Java с помощью JUnit (есть версия и с C#-Unit… но у меня есть такая… по большей части она независима. Рекомендуется.)

Хорошие тесты должны быть ПУТЕШЕСТВИЕМ (аббревиатура не достаточно липкая - у меня есть распечатка шпаргалки в книге, которую я должен был вытащить, чтобы убедиться, что я правильно понял…)

  • Автоматически: вызов тестов, а также проверка результатов PASS/FAIL должны выполняться автоматически
  • Тщательное: покрытие; Хотя ошибки, как правило, группируются вокруг определенных областей в коде, убедитесь, что вы протестировали все ключевые пути и сценарии. Используйте инструменты, если вам нужно знать непроверенные области
  • Повторяемость: тесты должны давать одинаковые результаты каждый раз... каждый раз. Тесты не должны опираться на неконтролируемые параметры.
  • Независимо: очень важно.
    • Тесты должны проверять только одну вещь за раз. Несколько утверждений в порядке, если они все тестируют одну функцию / поведение. Если тест не пройден, он должен точно определить местонахождение проблемы.
    • Тесты не должны полагаться друг на друга - изолированные. Никаких предположений о порядке выполнения теста. Убедитесь, что "чистый лист" перед каждым тестом, используя соответствующие настройки / демонтажа
  • Профессиональный: в конечном итоге вы получите столько же тестового кода, сколько и рабочего (если не больше), поэтому следуйте тому же стандарту хорошего дизайна для своего тестового кода. Хорошо продуманные методы-классы с намеренно раскрывающими именами, без дублирования, тесты с хорошими именами и т. Д.

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

Обновление 2010-08:

  • Удобочитаемость: это можно считать частью Professional - однако это не может быть подчеркнуто достаточно. Кислотным тестом было бы найти кого-то, кто не является частью вашей команды, и попросить его / ее выяснить тестируемое поведение в течение нескольких минут. Тесты должны поддерживаться так же, как производственный код, поэтому их легко читать, даже если это требует больше усилий. Тесты должны быть симметричными (следовать шаблону) и краткими (тестировать по одному поведению за раз). Используйте согласованное соглашение об именах (например, стиль TestDox). Избегайте загромождения теста "случайными деталями".. станьте минималистом.

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

  1. Не пишите ужасных тестов. Как предполагает "единица" в "модульном тесте", сделайте каждый элемент атомарным и изолированным, насколько это возможно. Если необходимо, создайте предварительные условия, используя фиктивные объекты, а не создавайте вручную слишком большую часть типичной пользовательской среды.
  2. Не проверяйте вещи, которые явно работают. Старайтесь не тестировать классы от сторонних поставщиков, особенно те, которые предоставляют основные API-интерфейсы платформы, в которой вы кодируете. Например, не тестируйте добавление элемента в класс Hashtable вендора.
  3. Подумайте об использовании инструмента покрытия кода, такого как NCover, чтобы помочь обнаружить крайние случаи, которые вам еще предстоит протестировать.
  4. Попробуйте написать тест до реализации. Думайте о тесте как о спецификации, которой будет придерживаться ваша реализация. Ср также поведенческая разработка, более специфическая ветвь управляемой тестированием разработки.
  5. Быть последовательным. Если вы пишете тесты только для некоторого кода, это вряд ли полезно. Если вы работаете в команде, а некоторые или все остальные не пишут тесты, это тоже не очень полезно. Убедите себя и всех остальных в важности (и свойствах, экономящих время) тестирования или не беспокойтесь.

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

Womp's 5 Законов Письменных Тестов:


1. Используйте длинные описательные имена методов испытаний.

   - Map_DefaultConstructorShouldCreateEmptyGisMap()
   - ShouldAlwaysDelegateXMLCorrectlyToTheCustomHandlers()
   - Dog_Object_Should_Eat_Homework_Object_When_Hungry()

2. Напишите свои тесты в стиле Arrange/Act/Assert.

  • Хотя эта организационная стратегия существовала некоторое время и называла многие вещи, введение аббревиатуры "AAA" в последнее время стало отличным способом объяснить это. Приведение всех ваших тестов в соответствие со стилем AAA облегчает их чтение и обслуживание.

3. Всегда предоставляйте сообщение об ошибке с вашими утверждениями.

Assert.That(x == 2 && y == 2, "An incorrect number of begin/end element 
processing events was raised by the XElementSerializer");
  • Простая, но полезная практика, которая делает очевидным в вашем приложении, что не удалось. Если вы не предоставляете сообщение, вы обычно получаете что-то вроде "Ожидаемое истина, ложь" в выходных данных об ошибке, что заставляет вас действительно идти читать тест, чтобы выяснить, в чем дело.

4. Прокомментируйте причину теста - какова бизнес-предпосылка?

  /// A layer cannot be constructed with a null gisLayer, as every function 
  /// in the Layer class assumes that a valid gisLayer is present.
  [Test]
  public void ShouldNotAllowConstructionWithANullGisLayer()
  {
  }
  • Это может показаться очевидным, но эта практика защитит целостность ваших тестов от людей, которые в первую очередь не понимают причину теста. Я видел, как многие тесты удалялись или модифицировались, что было совершенно нормально, просто потому, что человек не понимал предположений, которые тест проверял.
  • Если тест является тривиальным или имя метода достаточно наглядно, можно оставить комментарий неактивным.

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

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

Помните об этих целях (адаптировано из книги Meszaros Test Patterns xUnit)

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

Некоторые вещи, чтобы сделать это проще:

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

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

Тесты должны быть изолированными. Один тест не должен зависеть от другого. Более того, тест не должен опираться на внешние системы. Другими словами, тестируйте свой код, а не код, от которого зависит ваш код. Вы можете тестировать эти взаимодействия как часть ваших интеграционных или функциональных тестов.

Некоторые свойства отличных юнит-тестов:

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

  • При рефакторинге ни один тест не должен провалиться.

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

  • Все тесты должны проходить всегда; нет недетерминированных результатов.

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

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


РЕДАКТИРОВАТЬ: был комментарий о дублировании, вызванном "одно утверждение на тест". В частности, если у вас есть некоторый код для настройки сценария, а затем вы хотите сделать несколько утверждений об этом, но иметь только одно утверждение на тест, вы можете дублировать настройку в нескольких тестах.

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

[TestFixture]
public class StackTests
{
    [TestFixture]
    public class EmptyTests
    {
        Stack<int> _stack;

        [TestSetup]
        public void TestSetup()
        {
            _stack = new Stack<int>();
        }

        [TestMethod]
        [ExpectedException (typeof(Exception))]
        public void PopFails()
        {
            _stack.Pop();
        }

        [TestMethod]
        public void IsEmpty()
        {
            Assert(_stack.IsEmpty());
        }
    }

    [TestFixture]
    public class PushedOneTests
    {
        Stack<int> _stack;

        [TestSetup]
        public void TestSetup()
        {
            _stack = new Stack<int>();
            _stack.Push(7);
        }

        // Tests for one item on the stack...
    }
}

То, что вам нужно, - это определение поведения тестируемого класса.

  1. Проверка ожидаемого поведения.
  2. Проверка случаев ошибки.
  3. Охват всех путей кода в классе.
  4. Осуществление всех функций-членов в классе.

Основная цель - повысить вашу уверенность в поведении класса.

Это особенно полезно, когда вы смотрите на рефакторинг вашего кода. У Мартина Фаулера есть интересная статья о тестировании на его веб-сайте.

НТН.

веселит,

обкрадывать

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

Хорошие тесты должны быть ремонтопригодными.

Я не совсем понял, как это сделать для сложных сред.

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

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

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

Вот где вы начинаете иметь дело с компромиссами:

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

Вам также необходимо решить:

где вы храните контрольные примеры в вашей кодовой базе?

  • как вы документируете свои тесты?
  • Можно ли повторно использовать контрольные приборы для сохранения обслуживания тестового кейса?
  • что происходит, когда выполнение ночного теста завершается неудачно? Кто делает сортировку?
  • Как вы поддерживаете ложные объекты? Если у вас есть 20 модулей, каждый из которых использует свой собственный интерфейс API журнального макета, быстрое изменение API-интерфейса. Меняются не только контрольные примеры, но и 20 фиктивных объектов. Эти 20 модулей были написаны в течение нескольких лет многими разными командами. Это классическая проблема повторного использования.
  • люди и их команды понимают ценность автоматизированных тестов, им просто не нравится, как это делает другая команда.:-)

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

Тесты должны быть ремонтопригодными.

Мне нравится аббревиатура Right BICEP из вышеупомянутой книги " Прагматическое модульное тестирование":

  • Правильно: результаты правильные?
  • Б: Все ли граничные условия верны?
  • Я: Можем ли мы проверить обратные отношения?
  • C: Можем ли мы проверить результаты с помощью других средств?
  • E: Можем ли мы вызвать условия ошибки?
  • П: Находятся ли характеристики производительности в определенных пределах?

Лично я чувствую, что вы можете продвинуться довольно далеко, проверив, что вы получите правильные результаты (1+1 должен возвращать 2 в дополнительной функции), опробовав все граничные условия, которые вы можете придумать (например, используя два числа, из которых сумма больше целочисленного максимального значения в функции добавления) и форсирует условия ошибок, такие как сбои сети.

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

Я определяю "хорошие" модульные тесты, если они обладают следующими тремя свойствами:

  • Они читабельны (наименования, утверждения, переменные, длина, сложность..)
  • Они Поддерживаемые (без логики, не переопределены, основаны на состоянии, реорганизованы...)
  • Они заслуживают доверия (тестируйте правильные вещи, изолированные, а не интеграционные тесты..)
  • Модульное тестирование просто проверяет внешний API вашего модуля, вы не должны тестировать внутреннее поведение.
  • Каждый тест TestCase должен тестировать один (и только один) метод внутри этого API.
    • Дополнительные случаи испытаний должны быть включены для случаев неудачи.
  • Проверьте охват ваших тестов: после того, как юнит протестирован, должны быть выполнены 100% строк внутри этого юнита.

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

С уважением

Я второй ответ "ПОЕЗДКА", за исключением того, что тесты должны полагаться друг на друга!!!

Зачем?

СУХОЙ - не повторяйся - относится и к тестированию! Тестовые зависимости могут помочь: 1) сэкономить время установки, 2) сохранить ресурсы устройства и 3) точно определить сбои. Конечно, только с учетом того, что ваша инфраструктура тестирования поддерживает первоклассные зависимости. В противном случае, я признаю, они плохие.

Продолжить http://www.iam.unibe.ch/~scg/Research/JExample/

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

Часто модульные тесты основаны на фиктивном объекте или фиктивных данных. Мне нравится писать три вида модульных тестов:

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

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

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

Подумайте о двух типах тестирования и рассмотрите их по-разному: функциональное тестирование и тестирование производительности.

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

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

    Первый раздел имени теста - это имя метода в тестируемой системе.
    Далее идет конкретный сценарий, который тестируется.
    Наконец, результаты этого сценария.

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

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

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

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