Почему модульные тесты проверяют только одно?

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

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

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

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

Ответ 1

Я собираюсь выйти на конечность здесь и сказать, что совет "только одно испытание" не так полезен, как это иногда бывает.

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

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

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

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

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

Ответ 2

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

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

Ответ 3

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

[Edit:] Хорошо, скажем, это образец тестового метода:

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

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

Ответ 4

Haaa... unit tests.

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

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

Есть

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

лучше, чем

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

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

Но

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

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

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

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

Мои 2 цента.

Ответ 5

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

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

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

Ответ 6

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

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

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

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

Хорошие характеристики unit test:

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

Ответ 7

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

Здесь должен быть иллюстративный пример. Скажем, у вас есть класс стека с запросами:

  • GETSIZE
  • IsEmpty
  • getTop

и методы для мутации стека

  • толчок (anObject)
  • поп()

Теперь рассмотрим следующий тестовый пример (я использую 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() - это запросы, которые тестируются отдельными методами тестирования.

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

Ответ 8

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

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

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

Ответ 9

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

Ответ 10

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

Ответ 11

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

Ответ 12

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

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

Ответ 13

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

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);
    }
  }
}

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

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

Ответ 14

Другим практическим недостатком очень гранулированного модульного тестирования является то, что он нарушает принцип DRY. Я работал над проектами, где правило состояло в том, что каждый открытый метод класса должен иметь unit test ([TestMethod]). Очевидно, это добавило некоторые накладные расходы каждый раз, когда вы создали публичный метод, но реальной проблемой было то, что он добавил некоторые "трения" к рефакторингу.

Это похоже на документацию по уровню метода, это хорошо, но это еще одна вещь, которая должна поддерживаться, и это делает изменение подписи или имени метода немного более громоздким и замедляет "рефакторинг" (как описано в "Инструменты рефакторинга: пригодность для цели" от Emerson Murphy-Hill и Andrew P. Black. PDF, 1.3 MB).

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

Ответ 15

Когда тест не выполняется, существует три варианта:

  • Реализация не работает и должна быть исправлена.
  • Тест сломан и должен быть исправлен.
  • Тест больше не нужен и должен быть удален.

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

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

Вот что это может выглядеть (с 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())
    })
  })
}

Ответ 16

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

Ну, так что, когда какой-то тест не удается, вы знаете, какой метод терпит неудачу.

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

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

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

Ответ 17

Отказ от ответственности: на этот вопрос большое влияние оказала книга "xUnit Test Patterns".

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

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

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

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