TDD: Почему там только один тест на функцию?

Мне нелегко понимать, почему существует только один тест на функцию в самом профессиональном коде TDD, который я видел. Когда я приблизился к TDD первоначально, я склонялся к тестам группы 4-5 на функцию, если они были связаны, но я вижу, что это, кажется, не стандарт. Я знаю, что это является более описательным, чтобы иметь всего один тест на функцию, потому что можно более легко сузить, какова проблема, но я борюсь для предложения имен функций для дифференциации различных тестов, так как многие так подобны.

Таким образом, мой вопрос: это - действительно плохая практика для помещения нескольких тестов в одну функцию и раз так почему? Там существует ли согласие? Спасибо

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

Большие Ответы. Собираясь быть твердым выбрать победителя

10
задан John Baker 26 December 2009 в 02:22
поделиться

8 ответов

похоже, что вы спрашиваете "почему в большинстве профессиональных TDD-кодов, которые я видел, есть только одно утверждение за тест". Скорее всего, это делается для того, чтобы увеличить изоляцию теста, а также покрытие теста при наличии сбоев. Безусловно, именно поэтому я создал свою библиотеку TDD (для PHP) таким образом. Допустим, у вас есть

function testFoo()
{
    $this->assertEquals(1, foo(10));
    $this->assertEquals(2, foo(20));
    $this->assertEquals(3, foo(30));
}

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

.
12
ответ дан 3 December 2019 в 13:40
поделиться

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

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

.
3
ответ дан 3 December 2019 в 13:40
поделиться

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

Дискрипторность в тестах хороша. Если вы собираетесь писать 5 тестов, то иметь каждый из них в своей функции кажется не сложнее, чем иметь их все в одном месте, за исключением незначительных накладных расходов на создание новой функции boilerplate каждый раз. С правильной IDE, даже это может быть проще, чем копировать и вставлять.

.
2
ответ дан 3 December 2019 в 13:40
поделиться

Да, в TDD необходимо проверить одно поведение каждой функции. Вот почему.

  1. Если ты пишешь свои тесты до того, как ты кодируешь, то множественное поведение, протестированное в одной функции, означает, что ты реализуешь множественное поведение за один раз, что плохо.
  2. Одно поведение, протестированное в одной функции, означает, что в случае неудачи теста, ты знаешь точно , почему он провалился, и может обнуляться по конкретной проблемной области. Если в одной функции тестировалось несколько поведений, то сбой в "более позднем" тесте может быть связан с тем, что в более раннем тесте сбой не был заявлен и привел к плохому состоянию.
  3. Одно поведение, протестированное на функцию, означает, что если это поведение когда-либо нужно будет переопределить, то нужно беспокоиться только о специфических для этого поведения тестах, а не о других, не связанных между собой тестах (ну, во всяком случае, не из-за компоновки тестов...)

И последний вопрос - почему не имеют по одному тесту на каждую функцию? В чём выгода? Я не думаю, что есть налог на декларации функций.

9
ответ дан 3 December 2019 в 13:40
поделиться

Высокая гранулометричность тестов рекомендуется не только для удобства идентификации проблем, но и потому, что секвенирование тестов внутри функции может случайно скрыть проблемы. Предположим, например, что вызов метода foo с аргументом bar должен возвращать 23 -- но из-за ошибки в том, как объект инициализирует свое состояние, он возвращает 42, если вызывается как самый первый метод на вновь построенном объекте (после этого он корректно переключается на возврат 23). Если ваш тест foo не придет сразу после создания объекта, то вы пропустите эту проблему, а если вы будете группировать тесты по 5 за раз, то у вас будет только 20% шанс, что вы случайно все сделаете правильно. С одним тестом на каждую функцию (и установкой/установкой, которая сбрасывает и перестраивает всё чисто каждый раз, конечно же), вы сразу же прикончите ошибку. Теперь это искусственно простая проблема только из соображений экспозиции, но общая проблема - что тесты не должны влиять друг на друга, а часто будут влиять, если только каждый из них не будет заключён в скобки с помощью настройки и разрушения функциональности - имеет большой резонанс.

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

.
6
ответ дан 3 December 2019 в 13:40
поделиться

Мне трудно понять, почему в большинстве профессиональных TDD-кодов, которые я видел

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

Public Function DoSomething(ByVal foo as Boolean) As Integer
   Dim result as integer = 0     

   If(foo) then
        result = MakeRequestToWebServiceA()
   Else
        result = MakeRequestToWebServiceB()
   End If     

   return result
End Function

В данном случае есть 2 'use case' или потока управления, которые может взять функция. Для этой функции должно быть как минимум 2 теста. Тот, который принимает foo как true и веток вниз по коду if(true), и тот, который принимает foo как false и идет вниз по второй ветке. Если у вас есть больше, если утверждения или потоки, то код может идти, но для этого потребуется большее количество тестов. Это связано с несколькими причинами - самая важная для меня - без нее тесты были бы слишком сложными и трудночитаемыми. Есть и другие причины, как, например, в случае с приведенной выше функцией, поток управления основан на входном параметре - а это значит, что для проверки всех путей кода необходимо дважды вызвать функцию. Никогда не стоит вызывать функцию больше одного раза, когда тестируешь в IMO.

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

Может быть, ты слишком много думаешь об этом? Не бойтесь писать сумасшедшие, слишком многословные имена для вашей тестовой функции. Что бы ни делал этот тест, пишите его на английском языке, используйте подчеркивания и придумывайте набор стандартов для имен, чтобы кто-нибудь другой, глядя на код (в том числе и вы через 6 месяцев), мог легко понять, что он делает. Помните, что на самом деле вам никогда не придётся вызывать эту функцию самостоятельно (по крайней мере, в большинстве тестовых фреймворков), так что кому какое дело, если её название будет 100 символов. С ума сойти. В приведенном выше примере мои 2 теста будут названы:

 DoSomethingTest_TestWhenFooIsTrue_RequestIsMadeToWebServiceA()
 DoSomethingTest_TestWhenFooIsFalse_RequestIsMadeToWebServiceB()

Также - это всего лишь общее руководство. Есть определённые случаи, когда в одном юнит-тесте будет несколько утверждений. Это произойдет при тестировании управляющего потока того же , но при написании утверждения (утверждений) необходимо проверить несколько полей. Возьмем, к примеру, этот тест - тест для функции, которая разбирает CSV-файл в бизнес-объект, имеющий поля Header, a Body и Footer:

 Public Sub ParseFileTest_TestFileIsParsedCorrectly()
        Dim target as new FileParser()
        Dim actual as SomeBusinessObject = target.ParseFile(TestHelper.GetTestData("ParseFileTest.csv")))

        Assert.Equals(actual.Header,"EXPECTED HEADER FROM TEST DATA FILE")
        Assert.Equals(actual.Footer,"EXPECTED FOOTER FROM TEST DATA FILE")
        Assert.Equals(actual.Body,"TEST DATA BODY")
 End Sub

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

-Drew

6
ответ дан 3 December 2019 в 13:40
поделиться

Я думаю, что хороший способ - не думать в терминах количества тестов на функцию, а думать в терминах покрытия кода :

  • Покрытие функции - имеет каждую функцию (или подпрограмму) в
    . программа вызывалась?
  • Покрытие оператора - Был ли каждый узел в программе
    ? Выполнено?
  • Покрытие ветки - Был ли каждый фронт в программе
    . Выполнено?
  • Покрытие решения - Имеет ли каждая структура управления (например, IF утверждение) оценивалось как истинное, так и ложное?
  • Покрытие условий - было ли оценено каждое булевое подвыражение. и к правде, и к лжи? Это не так. Обязательно подразумевает покрытие решения.
  • Покрытие условий/решений - Как решение, так и покрытие условий
    . должно быть удовлетворено.

EDIT : Я перечитал то, что написал, и нашел это "страшным" ... что напомнило мне хорошую мысль, которую я слышал несколько недель о покрытии кода :

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

3
ответ дан 3 December 2019 в 13:40
поделиться

Считайте, что этот соломенный человек (на C#)

void FooTest()
{
    C c = new C();
    c.Foo();
    Assert(c.X == 7);
    Assert(c.Y == -7);
}

Хотя "одно утверждение на каждую тестовую функцию" является хорошим советом по TDD, оно неполное. Одно только его применение дало бы:

void FooTestX()
{
    C c = new C();
    c.Foo();
    Assert(c.X == 7);
}

void FooTestY()
{
    C c = new C();
    c.Foo();
    Assert(c.X == 7);
}

Не хватает двух вещей: Once-and-only-once (он же DRY), и "по одному тестовому классу на сценарий". Последнее является менее известным: вместо одного тестового класса / тестового приспособления, в котором хранятся все методы тестирования, вложены классы для нетривиальных сценариев. Вот так:

class CTests
{
    class FooTests
    {
        readonly C c;

        void Setup()
        {
            c = new C();
            c.Foo();
        }

        void XTest()
        {
            Assert(c.X == 7);
        }

        void YTest()
        {
            Assert(c.Y == -7);
        }
    }
}

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

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

(Я избегал деталей, специфичных для технологии юнит-тестов, например, NUnit или MSTest. Вам придется подстраиваться под то, что вы используете, но принципы звучат.)

(Я избегал подробностей, специфичных для технологии юнит-тестирования, например, NUnit или MSTest.
2
ответ дан 3 December 2019 в 13:40
поделиться
Другие вопросы по тегам:

Похожие вопросы: