Можно ли иметь несколько утверждений в одном модульном тесте?

В комментарии к этой замечательной должности Рой Ошерове упомянутый проект OAPT , который предназначен для запуска каждого утверждения в одном тесте.

На домашней странице проекта написано следующее:

  

Надлежащие модульные тесты должны   именно одна причина, , почему вы   должен использовать одно утверждение на единицу   тест.

И, кроме того, Рой написал в комментариях:

  

Мое правило обычно заключается в том, что вы проверяете   один логический КОНЦЕПТ за тест. ты можешь   имеют несколько утверждений на одном и том же    объект . они будут, как правило, проверять те же самые понятия.

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

314 голосов | спросил 4 revs, 3 users 74%
Restuta
1 Jam1000000amThu, 01 Jan 1970 03:00:00 +030070 1970, 03:00:00

14 ответов


190

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

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

Как вы решаете несколько утверждений?

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
219

Тесты должны выполняться только по одной причине, но это не всегда означает, что должен быть только один оператор Assert. ИМХО, более важно придерживаться шаблона Упорядочить, действовать, утверждать ".

Ключ в том, что у вас есть только одно действие, а затем вы проверяете результаты этого действия с помощью утверждений. Но это «Arrange, Act, Assert, End of test ». Если у вас возникли соблазны продолжить тестирование, выполнив другое действие и после этого добавив больше утверждений, сделайте вместо этого отдельный тест.

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

 [Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 

или

 [Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 

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

например. Первый мог быть

 Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 

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

NB . Код, который я пишу здесь, это C # с NUnit, но принципы будут поддерживаться другими языками и фреймворками. Синтаксис тоже может быть очень похож.

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
78

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

Я делаю это все время:

 public void ToPredicateTest()
{
    ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100);
    Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400)
                                                       .Or()
                                                       .OpenParenthesis()
                                                       .GreaterThanConst(500)
                                                       .And()
                                                       .LessThanConst(1000)
                                                       .And().Not()
                                                       .EqualsConst(666)
                                                       .CloseParenthesis()
                                                       .ToPredicate();
    Assert.IsTrue(p(ResultField.FillResult(rf, 399)));
    Assert.IsTrue(p(ResultField.FillResult(rf, 567)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 400)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 666)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 1001)));

    Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate();

    Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true)));
    Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false)));
}

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

Я тестирую только одну единицу (метод ToPredicate), но я охватываю все, что я могу придумать в тесте.

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
17

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

 @Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}

Он представляет условия, которые должны существовать на каждом шаге процесса, чтобы я был уверен, что код ведет себя так, как я ожидаю. Если одно утверждение терпит неудачу, мне все равно, что оставшиеся даже не будут запущены; потому что состояние системы больше недействительно, эти последующие утверждения не скажут мне ничего ценного. * Если assertAllUnitsAlerting() не удалось, тогда я не знаю, что сделать из assertAllNotificationSent() или сбой до тех пор, пока я не определю, что вызывало предыдущую ошибку и исправило ее.

(* - Хорошо, они могли бы быть полезны при отладке проблемы. Но самая важная информация о том, что тест не прошел, уже получен.)

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
8

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

 class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

В моем тесте я просто хочу проверить, что service.process() возвращает правильный номер в экземплярах класса Inner.

Вместо тестирования ...

 @Test
public void test() {
    Result res = service.process();
    if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() );
}

Я делаю

 @Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
6

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

Например, представьте себе функцию, которая анализирует строку даты:

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}

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

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
2

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

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

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
2

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

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);

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

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
2

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

 @Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}

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

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
2

Я не знаю ни одной ситуации, когда было бы неплохо иметь несколько утверждений внутри самого метода [Test]. Основная причина, по которой людям нравится иметь несколько утверждений, заключается в том, что они пытаются иметь один класс [TestFixture] для каждого тестируемого класса. Вместо этого вы можете разбить свои тесты на несколько классов [TestFixture]. Это позволяет вам увидеть несколько способов, с помощью которых код, возможно, не реагировал так, как вы ожидали, вместо того, чтобы просто сбой первого утверждения. Как вы это достигаете, у вас есть по крайней мере один каталог для каждого класса, который тестируется с большим количеством классов [TestFixture] внутри. Каждый класс [TestFixture] будет назван в честь определенного состояния объекта, который вы будете тестировать. Метод [SetUp] получит объект в состояние, описываемое именем класса. Затем у вас есть несколько методов [Test], каждый из которых утверждает разные вещи, которые вы ожидаете быть истинными, учитывая текущее состояние объекта. Каждый метод [Test] назван в честь того, что он утверждает, за исключением того, что он может быть назван в честь концепции, а не только для чтения на английском языке. Затем каждой реализации метода [Test] требуется только одна строка кода, где она что-то утверждает. Другим преимуществом этого подхода является то, что он делает тесты очень читабельными, так как становится совершенно ясно, что вы тестируете, и чего вы ожидаете, просто глядя на имена классов и методов. Это также будет улучшаться по мере того, как вы начнете понимать все случаи с маленькими краями, которые вы хотите протестировать, и когда вы найдете ошибки.

Обычно это означает, что конечная строка кода внутри метода [SetUp] должна хранить значение свойства или возвращаемое значение в переменной частного экземпляра в [TestFixture]. Тогда вы можете утверждать несколько разных вещей об этой переменной экземпляра из разных методов [Test]. Вы также можете сделать утверждения о том, какие разные свойства тестируемого объекта установлены сейчас, когда они находятся в желаемом состоянии.

Иногда вам нужно делать утверждения по пути, когда вы проверяете объект в нужном состоянии, чтобы убедиться, что вы не испортили работу перед тем, как вставить объект в нужное состояние. В этом случае эти дополнительные утверждения должны появляться внутри метода [SetUp]. Если что-то пойдет не так внутри метода [SetUp], будет ясно, что что-то не так с тестом перед тем, как объект когда-либо попал в желаемое состояние, которое вы хотели проверить.

Еще одна проблема, с которой вы можете столкнуться, заключается в том, что вы можете протестировать Исключение, которое вы ожидали получить. Это может соблазнить вас не следовать приведенной выше модели. Тем не менее, это может быть достигнуто путем исключения исключения из метода [SetUp] и сохранения его в переменной экземпляра. Это позволит вам утверждать разные вещи об исключении, каждый в своем собственном [Test] методе. Затем вы можете также утверждать другие вещи об исследуемом объекте, чтобы убедиться, что не было никаких непредвиденных побочных эффектов от вызываемого исключения.

Пример (это будет разбито на несколько файлов):

 namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
1

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

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

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
0

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

  

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

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

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

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

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
-1

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

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

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

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18
-3

Я даже не согласен с «неудачей только по одной причине». Что более важно, так это то, что тесты короткие и четко читаются.

Это не всегда достижимо, хотя и когда тест сложный (длинное) описательное имя и тестирование меньшего количества вещей имеет смысл.

ответил dthorpe 26 AM000000100000001831 2011, 10:58:18

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

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132