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


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

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

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

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

17 55

17 ответов:

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

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

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

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

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

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

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

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

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

[редактирование:] Хорошо, скажем, это пример метода тестирования:

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

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

хааа... модульное тестирование.

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

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

- Это

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

лучше, чем

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

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

но

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

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

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

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

мои 2 цента.

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

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

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

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

хорошие характеристики модульного теста:

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

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

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

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

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

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

  • getSize
  • isEmpty
  • getTop

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

  • push (anObject)
  • pop ()

Теперь рассмотрим следующий тестовый случай для этого (я использую 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"
как разработка через тестирование. Я лично пишу большие "функциональные тесты", которые сначала тестируют несколько методов, а затем создают модульные тесты, когда я начинаю реализовывать отдельные части.

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

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

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

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

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

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

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

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

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

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

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

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

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

в то же время, я действительно хотел увидеть все мои утверждения о том, что удалось, а не только первый. Я ожидал, что все они потерпят неудачу, и мне нужно было знать, какие суммы я действительно получаю обратно. Но стандартная [настройка] с каждым разделенным тестом приведет к 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 Скотт Bellware это: http://code.google.com/p/specunit-net/

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

Это похоже на документацию уровня метода, это приятно иметь, но это другое дело, что должен поддерживаться, и это делает изменение подписи или имени метода немного более громоздким и замедляет "рефакторинг floss" (как описано в "инструменты рефакторинга: пригодность для использования" Эмерсон Мерфи-Хилл и Эндрю П. Блэк. PDF, 1.3 MB).

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

при сбое теста возможны три варианта:

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

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

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

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

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

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

отказ от ответственности: это ответ, сильно повлиявший на книгу "тестовые Шаблоны xUnit".

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

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