TDD: Почему только один тест для каждой функции? - PullRequest
10 голосов
/ 26 декабря 2009

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

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

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

Отличные ответы. Будет трудно выбрать победителя

Ответы [ 8 ]

12 голосов
/ 26 декабря 2009

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

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

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

9 голосов
/ 26 декабря 2009

Да, вы должны проверить одно поведение на функцию в TDD. И вот почему.

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

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

6 голосов
/ 26 декабря 2009

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

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

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 «варианта использования» или управляющих потока. Эта функция должна иметь как минимум 2 теста для нее. Тот, который принимает foo как true и разветвляет код if (true), и тот, который принимает foo как false и переходит во вторую ветвь. Если у вас есть больше, если операторы или потоки кода могут идти, тогда это потребует больше тестов. Это по нескольким причинам. Самым важным для меня является то, что без него тесты были бы слишком сложными и трудными для чтения. Есть и другие причины, например, в случае вышеупомянутой функции поток управления основан на входном параметре, что означает, что вы должны дважды вызвать функцию, чтобы протестировать все пути кода. Вам никогда не следует вызывать функцию более одного раза, которую вы тестируете в своем тестовом IMO.

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

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

 DoSomethingTest_TestWhenFooIsTrue_RequestIsMadeToWebServiceA()
 DoSomethingTest_TestWhenFooIsFalse_RequestIsMadeToWebServiceB()

Также - это всего лишь общее руководство. Определенно есть случаи, когда у вас будет несколько подтверждений в одном модульном тесте. Это произойдет, когда вы тестируете один и тот же поток управления 1018 *, но при написании утверждений assert необходимо проверить несколько полей. Возьмем, к примеру, тест для функции, которая анализирует CSV-файл в бизнес-объекте с полями Header, 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 голосов
/ 26 декабря 2009

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

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

3 голосов
/ 26 декабря 2009

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

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

РЕДАКТИРОВАТЬ: Я перечитал то, что я написал, и я нашел это немного «страшно» ... это напоминает мне хорошую мысль, которую я слышал несколько недель о покрытии кода:

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

3 голосов
/ 26 декабря 2009

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

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

2 голосов
/ 26 декабря 2009

Рассмотрим этого соломенного человека (в 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);
}

В нем отсутствуют две вещи: «Один-единственный-один-один раз» (он же 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. Вам придется подстраиваться под то, что вы используете, но принципы здравы.)

2 голосов
/ 26 декабря 2009

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...