Должен ли он быть «Упорядочить-Утвердить-Действовать-Утвердить»?


94

Что касается классического тестового шаблона Arrange-Act-Assert , я часто добавляю контрутверждение, которое предшествует Act. Таким образом, я знаю, что проходящее утверждение действительно проходит как результат действия.

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

Следуют ли ваши тесты этой схеме? Почему или почему нет?

Уточнение обновления : первоначальное утверждение по существу противоположно окончательному утверждению. Это не утверждение, что Arrange сработал; это утверждение, что Закон еще не сработал.

Ответы:


121

Это не самое обычное дело, но все же достаточно распространено, чтобы иметь собственное название. Этот метод называется Guard Assertion . Вы можете найти подробное описание этого на странице 490 в отличной книге xUnit Test Patterns Джерарда Месароша (настоятельно рекомендуется).

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

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


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

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

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

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

32

Она также может быть определен как расположе- Предположим -act-Утверждай.

В NUnit есть техническая поддержка для этого, как в примере здесь: http://nunit.org/index.php?p=theory&r=2.5.7


1
Ницца! Мне нравится четвертая - иная - и точная - «А». Благодарность!
Карл Манастер,

+1, @Ole! Мне этот тоже нравится в некоторых особых случаях! Я попробую!
Джон Тоблер

8

Вот пример.

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
    range.encompass(7);
    assertTrue(range.includes(7));
}

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

Читая само по себе, assertTrue(range.includes(7));говорится: «Утвердите, что измененный диапазон включает 7». В контексте первого утверждения, в нем говорится: «Утверждают, что вызов encompass () приводит к включению 7. И поскольку encompass - это модуль, который мы тестируем, я думаю, что это имеет некоторую (небольшую) ценность.

Я принимаю свой ответ; Многие другие неверно истолковали мой вопрос как о тестировании установки. Я думаю, это немного другое.


Спасибо, что вернулся с примером, Карл. Хорошо, в красной части цикла TDD, пока encompass () действительно что-то не сделает; первое утверждение бессмысленно, это всего лишь дублирование второго. В зеленом цвете он начинает приносить пользу. Это приобретает смысл при рефакторинге. Было бы неплохо иметь среду UT, которая делает это автоматически.
philant 01

Предположим, вы TDD этот класс Range, не будет ли еще один неудачный тест, тестирующий Range ctor, когда вы его сломаете?
philant 01

1
@philippe: Я не уверен, что понимаю вопрос. Конструктор Range и includes () имеют свои собственные модульные тесты. Не могли бы вы уточнить, пожалуйста?
Карл Манастер,

Чтобы первое утверждение assertFalse (range.includes (7)) завершилось ошибкой, вам необходимо иметь дефект в конструкторе диапазона. Поэтому я хотел спросить, не сломаются ли тесты для конструктора Range одновременно с этим утверждением. А как насчет утверждения после действия для другого значения: например, assertFalse (range.includes (6))?
филант

1
Создание диапазона, на мой взгляд, предшествует функциям, подобным include (). Итак, хотя я согласен, только неисправный конструктор (или неисправный include ()) приведет к сбою этого первого утверждения, тест конструктора не будет включать вызов includes (). Да, все функции до первого утверждения уже протестированы. Но это первоначальное отрицательное утверждение что-то сообщает, и, на мой взгляд, что-то полезное. Даже если каждое такое утверждение проходит, когда оно изначально написано.
Карл Манастер,

7

Arrange-Assert-Act-AssertТест всегда может быть переработан в двух тестов:

1. Arrange-Assert

а также

2. Arrange-Act-Assert

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

Это дает более точную обратную связь о том, потерпел неудачу этап Arrange или Act, в то время как в оригинале Arrange-Assert-Act-Assertони объединены, и вам придется копать глубже и точно исследовать, какое утверждение не удалось и почему оно не удалось, чтобы узнать, не удалось ли провалилась договоренность или действие.

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

Наконец, имейте в виду, что всякий раз, когда вы видите похожие разделы Arrange в разных тестах, вы должны попытаться вытащить их в общие вспомогательные методы, чтобы ваши тесты были более СУХИМИ и более удобными в обслуживании в будущем.


3

Я сейчас этим занимаюсь. AAAA другого типа

Arrange - setup
Act - what is being tested
Assemble - what is optionally needed to perform the assert
Assert - the actual assertions

Пример теста обновления:

Arrange: 
    New object as NewObject
    Set properties of NewObject
    Save the NewObject
    Read the object as ReadObject

Act: 
    Change the ReadObject
    Save the ReadObject

Assemble: 
    Read the object as ReadUpdated

Assert: 
    Compare ReadUpdated with ReadObject properties

Причина в том, что ACT не содержит чтения ReadUpdated, потому что это не часть действия. Акт только меняется и спасает. Итак, на самом деле, ARRANGE ReadUpdated для утверждения, я вызываю ASSEMBLE для утверждения. Это сделано для того, чтобы не запутать раздел ARRANGE.

ASSERT должен содержать только утверждения. Остается ASSEMBLE между ACT и ASSERT, который устанавливает assert.

Наконец, если вы не справляетесь с Arrange, ваши тесты неверны, потому что у вас должны быть другие тесты, чтобы предотвратить / найти эти тривиальные ошибки. Поскольку для сценария, который я представляю, уже должны быть другие тесты, которые проверяют READ и CREATE. Если вы создаете «Утверждение защиты», вы можете нарушить DRY и создать обслуживание.


1

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


1

Я уже читал об этой технике - возможно, от вас, кстати, - но я ею не пользуюсь; в основном потому, что я привык к форме Triple A для своих модульных тестов.

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

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


Обычно я не заставляю начальное утверждение терпеть неудачу - в конце концов, оно не должно терпеть неудачу, как и утверждение TDD, до того, как будет написан его метод. Я же пишу это, когда я пишу это, перед тем , как раз в процессе написания теста, а не позже. Честно говоря, я не могу припомнить, чтобы это было неудачно - возможно, это означает, что это пустая трата времени. Я попытаюсь привести пример, но в настоящее время я его не имею. Спасибо за вопросы; они полезны.
Карл Манастер,

1

Я делал это раньше, когда исследовал неудачный тест.

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

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


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

1

В общем, мне очень нравится "Arrange, Act, Assert", и я использую его как свой личный стандарт. Однако единственное, о чем он не напоминает мне, - это дезорганизовать то, что я организовал, когда утверждения выполнены. В большинстве случаев это не вызывает особого раздражения, поскольку большинство вещей автоматически исчезают через сборку мусора и т. Д. Однако, если вы установили подключения к внешним ресурсам, вы, вероятно, захотите закрыть эти подключения, когда закончите. с вашими утверждениями, или у вас есть сервер или дорогой ресурс где-то там, где-то есть соединения или жизненно важные ресурсы, которые он должен быть в состоянии отдать кому-то другому. Это особенно важно, если вы один из тех разработчиков, которые не используют TearDown или TestFixtureTearDown.чтобы очистить после одного или нескольких тестов. Конечно, «Arrange, Act, Assert» не несет ответственности за то, что я не смог закрыть то, что я открываю; Я упоминаю об этой "ловушке" только потому, что я еще не нашел хорошего "A-word" синонима для "утилизации", чтобы рекомендовать! Какие-либо предложения?


1
@carlmanaster, ты для меня достаточно близок! Я вставляю это в свой следующий TestFixture, чтобы примерить размер. Это похоже на маленькое напоминание о том, что нужно делать то, чему вас должна была научить ваша мать: «Если вы откроете его, закройте! Если вы испортили, уберите!» Может быть, кто-то еще сможет улучшить его, но, по крайней мере, он начинается с "а!" Спасибо за предложение!
Джон Тоблер

1
@carlmanaster, я действительно попробовал "Аннуль". Это лучше, чем «разборка», и это вроде как работает, но я все еще ищу еще одно слово на букву «А», которое так же хорошо бы застряло в моей голове, как «Упорядочить, действовать, утверждать». Может быть "Уничтожить ?!"
Джон Тоблер

1
Итак, теперь у меня есть «Упорядочить, предположить, действовать, подтвердить, уничтожить». Хммм! Я слишком усложняю, да? Может, мне лучше просто прекратить KISS и вернуться к "Arrange, Act, and Assert!"
Джон Тоблер

1
Может быть, использовать R для сброса? Я знаю, что это не пятерка, но это звучит как пиратская поговорка: ааааааааааааааааааааааааааааааааалреда еще лучше и сбросить рифмы с Assert: o
Марсель Вальдес Ороско

1

Взгляните на статью Википедии о дизайне по контракту . Святая троица Arrange-Act-Assert - это попытка закодировать некоторые из тех же концепций и доказывает правильность программы. Из статьи:

The notion of a contract extends down to the method/procedure level; the
contract for each method will normally contain the following pieces of
information:

    Acceptable and unacceptable input values or types, and their meanings
    Return values or types, and their meanings
    Error and exception condition values or types that can occur, and their meanings
    Side effects
    Preconditions
    Postconditions
    Invariants
    (more rarely) Performance guarantees, e.g. for time or space used

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


0

Зависит от вашей среды / языка тестирования, но обычно, если что-то в части Arrange выходит из строя, генерируется исключение, и тест не отображает его вместо запуска части Act. Так что нет, я обычно не использую вторую часть Assert.

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


0

Я не использую этот шаблон, потому что думаю сделать что-то вроде:

Arrange
Assert-Not
Act
Assert

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

Используя пример вашего ответа:

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7)); // <-- Pointless and against DRY if there 
                                    // are unit tests for Range(int, int)
    range.encompass(7);
    assertTrue(range.includes(7));
}

Боюсь, вы не совсем поняли мой вопрос. Первоначальное утверждение не касается тестирования Arrange; это просто гарантия того, что Закон - это то, что приводит к утверждению состояния в конце.
Карл Манастер

И я хочу сказать, что все, что вы добавляете в часть Assert-Not, уже подразумевается в части Arrange, потому что код в части Arrange тщательно протестирован, и вы уже знаете, что он делает.
Марсель Вальдес Ороско,

Но я считаю, что часть Assert-Not имеет ценность, потому что вы говорите: учитывая, что часть Arrange оставляет «мир» в «этом состоянии», тогда мой «Акт» оставит «мир» в этом «новом состоянии». ; и если реализация кода, от которого зависит часть Arrange, изменится, то тест тоже сломается. Но опять же, это может быть против DRY, потому что у вас (должны) также быть тесты для любого кода, который вы используете в части Arrange.
Марсель Вальдес Ороско

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

Вероятно, такой пункт будет лучше в интеграционных тестах, системных тестах или приемочных тестах, где часть Arrange обычно зависит от более чем одного компонента, и есть больше факторов, которые могут вызвать неожиданное изменение исходного состояния «мира». Но я не вижу для этого места в модульных тестах.
Марсель Вальдес Ороско

0

Если вы действительно хотите протестировать все в примере, попробуйте еще тесты ... например:

public void testIncludes7() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
}

public void testIncludes5() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(5));
}

public void testIncludes0() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(0));
}

public void testEncompassInc7() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(7));
}

public void testEncompassInc5() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(5));
}

public void testEncompassInc0() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(0));
}

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

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


0

Я использую:

1. Setup
2. Act
3. Assert 
4. Teardown

Потому что чистая настройка очень важна.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.