Лучшая практика для отладки утверждений во время модульного тестирования - PullRequest
42 голосов
/ 04 января 2009

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

Edit: я обновил 'Assert' для отладки assert, чтобы отличить assert в тестируемом коде от строк в модульном тесте, которые проверяют состояние после запуска теста.

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

Ответы [ 11 ]

36 голосов
/ 30 июня 2010

Это совершенно правильный вопрос.

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

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

if (param1 == null)
    throw new ArgumentNullException("param1");

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

Теперь рассмотрим следующее:

if (param1 == null)
{
    Debug.Fail("param1 == null");
    throw new ArgumentNullException("param1");
}

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

Теперь, как мы справляемся с вашими юнит-тестами?

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

То, как вы это решите, будет зависеть от того, какие языки и т. Д. Вы используете. Тем не менее, у меня есть некоторые предложения, если вы используете .NET (я на самом деле не пробовал это, но я буду в будущем и обновлю пост):

  1. Проверьте Trace.Listeners. Найдите любой экземпляр DefaultTraceListener и установите для AssertUiEnabled значение false. Это останавливает модальное диалоговое окно от появления. Вы также можете очистить коллекцию слушателей, но вы не получите никакой трассировки.
  2. Напишите свой собственный TraceListener, который записывает утверждения. Как вы записываете утверждения, зависит от вас. Запись сообщения об ошибке может быть недостаточно хорошей, поэтому вы можете пройтись по стеку, чтобы найти метод, из которого получено утверждение, и записать его тоже.
  3. Как только тест закончится, убедитесь, что единственными ошибками утверждений были те, которые вы ожидали. Если возникли какие-либо другие, не пройдите тест.

В качестве примера TraceListener, который содержит код для такого обхода стека, я бы поискал SuperAssertListener SUPERASSERT.NET и проверил его код. (Также стоит интегрировать SUPERASSERT.NET, если вы действительно серьезно относитесь к отладке с использованием утверждений).

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

UPDATE:

Вот пример TraceListener, который можно использовать для проверки модульных утверждений. Вы должны добавить экземпляр в коллекцию Trace.Listeners. Вы, вероятно, также захотите предоставить какой-то простой способ, которым ваши тесты могут охватить слушателя.

ПРИМЕЧАНИЕ: это очень многим обязано SUPERASSERT.NET Джона Роббинса.

/// <summary>
/// TraceListener used for trapping assertion failures during unit tests.
/// </summary>
public class DebugAssertUnitTestTraceListener : DefaultTraceListener
{
    /// <summary>
    /// Defines an assertion by the method it failed in and the messages it
    /// provided.
    /// </summary>
    public class Assertion
    {
        /// <summary>
        /// Gets the message provided by the assertion.
        /// </summary>
        public String Message { get; private set; }

        /// <summary>
        /// Gets the detailed message provided by the assertion.
        /// </summary>
        public String DetailedMessage { get; private set; }

        /// <summary>
        /// Gets the name of the method the assertion failed in.
        /// </summary>
        public String MethodName { get; private set; }

        /// <summary>
        /// Creates a new Assertion definition.
        /// </summary>
        /// <param name="message"></param>
        /// <param name="detailedMessage"></param>
        /// <param name="methodName"></param>
        public Assertion(String message, String detailedMessage, String methodName)
        {
            if (methodName == null)
            {
                throw new ArgumentNullException("methodName");
            }

            Message = message;
            DetailedMessage = detailedMessage;
            MethodName = methodName;
        }

        /// <summary>
        /// Gets a string representation of this instance.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return String.Format("Message: {0}{1}Detail: {2}{1}Method: {3}{1}",
                Message ?? "<No Message>",
                Environment.NewLine,
                DetailedMessage ?? "<No Detail>",
                MethodName);
        }

        /// <summary>
        /// Tests this object and another object for equality.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            var other = obj as Assertion;

            if (other == null)
            {
                return false;
            }

            return
                this.Message == other.Message &&
                this.DetailedMessage == other.DetailedMessage &&
                this.MethodName == other.MethodName;
        }

        /// <summary>
        /// Gets a hash code for this instance.
        /// Calculated as recommended at http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return
                MethodName.GetHashCode() ^
                (DetailedMessage == null ? 0 : DetailedMessage.GetHashCode()) ^
                (Message == null ? 0 : Message.GetHashCode());
        }
    }

    /// <summary>
    /// Records the assertions that failed.
    /// </summary>
    private readonly List<Assertion> assertionFailures;

    /// <summary>
    /// Gets the assertions that failed since the last call to Clear().
    /// </summary>
    public ReadOnlyCollection<Assertion> AssertionFailures { get { return new ReadOnlyCollection<Assertion>(assertionFailures); } }

    /// <summary>
    /// Gets the assertions that are allowed to fail.
    /// </summary>
    public List<Assertion> AllowedFailures { get; private set; }

    /// <summary>
    /// Creates a new instance of this trace listener with the default name
    /// DebugAssertUnitTestTraceListener.
    /// </summary>
    public DebugAssertUnitTestTraceListener() : this("DebugAssertUnitTestListener") { }

    /// <summary>
    /// Creates a new instance of this trace listener with the specified name.
    /// </summary>
    /// <param name="name"></param>
    public DebugAssertUnitTestTraceListener(String name) : base()
    {
        AssertUiEnabled = false;
        Name = name;
        AllowedFailures = new List<Assertion>();
        assertionFailures = new List<Assertion>();
    }

    /// <summary>
    /// Records assertion failures.
    /// </summary>
    /// <param name="message"></param>
    /// <param name="detailMessage"></param>
    public override void Fail(string message, string detailMessage)
    {
        var failure = new Assertion(message, detailMessage, GetAssertionMethodName());

        if (!AllowedFailures.Contains(failure))
        {
            assertionFailures.Add(failure);
        }
    }

    /// <summary>
    /// Records assertion failures.
    /// </summary>
    /// <param name="message"></param>
    public override void Fail(string message)
    {
        Fail(message, null);
    }

    /// <summary>
    /// Gets rid of any assertions that have been recorded.
    /// </summary>
    public void ClearAssertions()
    {
        assertionFailures.Clear();
    }

    /// <summary>
    /// Gets the full name of the method that causes the assertion failure.
    /// 
    /// Credit goes to John Robbins of Wintellect for the code in this method,
    /// which was taken from his excellent SuperAssertTraceListener.
    /// </summary>
    /// <returns></returns>
    private String GetAssertionMethodName()
    {

        StackTrace stk = new StackTrace();
        int i = 0;
        for (; i < stk.FrameCount; i++)
        {
            StackFrame frame = stk.GetFrame(i);
            MethodBase method = frame.GetMethod();
            if (null != method)
            {
                if(method.ReflectedType.ToString().Equals("System.Diagnostics.Debug"))
                {
                    if (method.Name.Equals("Assert") || method.Name.Equals("Fail"))
                    {
                        i++;
                        break;
                    }
                }
            }
        }

        // Now walk the stack but only get the real parts.
        stk = new StackTrace(i, true);

        // Get the fully qualified name of the method that made the assertion.
        StackFrame hitFrame = stk.GetFrame(0);
        StringBuilder sbKey = new StringBuilder();
        sbKey.AppendFormat("{0}.{1}",
                             hitFrame.GetMethod().ReflectedType.FullName,
                             hitFrame.GetMethod().Name);
        return sbKey.ToString();
    }
}

Вы можете добавлять утверждения в коллекцию AllowedFailures в начале каждого теста для ожидаемых утверждений.

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

if (DebugAssertListener.AssertionFailures.Count > 0)
{
    // TODO: Create a message for the failure.
    DebugAssertListener.ClearAssertions();
    DebugAssertListener.AllowedFailures.Clear();
    // TODO: Fail the test using the message created above.
}
12 голосов
/ 25 августа 2014

ИМХО debug.asserts рок. В этой замечательной статье показано, как не дать им прервать ваш модульный тест, добавив app.config в проект модульного тестирования и отключив диалоговое окно:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.diagnostics>
    <assert assertuienabled="false"/>
</system.diagnostics>

7 голосов
/ 08 апреля 2010

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

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

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

Второй момент - это то, во что, похоже, попадают многие разработчики. Проверь юнит из всех вещей, с которыми работает твой код, и утверждай или выбрасывай исключения для всего остального - в конце концов, если твой код НЕ построен для того, чтобы справляться с этими ситуациями, и ты заставляешь их случаться, что вы ожидаете, что произойдет?
Вы знаете те части документации C / C ++, которые говорят о «неопределенном поведении»? Это оно. Под залог и под залог тяжело.


Обновление, чтобы уточнить: Обратной стороной этого является то, что вы в конечном итоге понимаете, что вы должны использовать Debug.Assert только для внутренних вещей, вызывающих другие внутренние вещи. Если ваш код доступен третьим сторонам (то есть, это библиотека или что-то еще), то нет предела тому, что вы можете ожидать от ввода, и, таким образом, вы должны проверять правильность и генерировать исключения или что-то еще, и вам также следует выполнить модульное тестирование

7 голосов
/ 04 января 2009

Утверждения в вашем коде - это (должны быть) операторы для читателя, которые говорят, что «это условие всегда должно выполняться на этом этапе» Сделанный с некоторой дисциплиной, они могут быть частью обеспечения правильности кода; большинство людей используют их как отладочные операторы печати. Модульные тесты - это код, который демонстрирует , что ваш код правильно выполняет определенный тестовый пример; что ж, они могут как документировать условия, так и повысить вашу уверенность в том, что код действительно правильный.

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

2 голосов
/ 04 января 2009

Хорошая юнит-тестовая установка будет способна ловить утверждения. Если активировано утверждение, текущий тест не пройден, а следующий запускается.

В наших библиотеках низкоуровневые функции отладки, такие как TTY / ASSERTS, имеют вызываемые обработчики. Обработчик по умолчанию будет printf / break, но клиентский код может устанавливать собственные обработчики для различного поведения.

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

Вы также можете включить проверку assert в свой юнит-тест - например,

CHECK_ASSERT (someList.getAt (someList.size () + 1); // тест проходит, если выполняется утверждение

1 голос
/ 25 августа 2013

Вы должны сохранять свои отладочные утверждения даже при наличии модульных тестов.

Проблема здесь не в том, чтобы различать ошибки и проблемы.

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

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

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

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

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

1 голос
/ 04 января 2009

Сначала для проведения модульных тестов и , предназначенных как для Проекта, ваш фреймворк для модульных тестов должен быть в состоянии уловить утверждения. Если ваши юнит-тесты прерываются из-за прерывания DbC, вы просто не сможете их запустить. Альтернативой здесь является отключение этих утверждений во время выполнения (чтение компиляции) ваших модульных тестов.

Поскольку вы тестируете непубличные функции, каков риск вызова функции с неверным аргументом? Разве ваши юнит-тесты не покрывают этот риск? Если вы пишете свой код в соответствии с методикой TDD (Test-Driven Development), они должны.

Если вы действительно хотите / нуждаетесь в этих утверждениях типа Dbc в своем коде, то вы можете удалить модульные тесты, которые передают недопустимые аргументы методам, имеющим эти утверждения.

Однако утверждения типа Dbc могут быть полезны в функциях более низкого уровня (которые не вызываются непосредственно модульными тестами), когда у вас есть грубые модульные тесты.

1 голос
/ 04 января 2009

Вы имеете в виду утверждения C ++ / Java для утверждений "программирования по контракту" или утверждения CppUnit / JUnit? Этот последний вопрос заставляет меня поверить, что это первый.

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

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

0 голосов
/ 09 января 2018

Не мешает ли интенсивное использование юнит-тестов утверждениям отладки?

Нет. Противоположный. Модульное тестирование делает утверждения Debug гораздо более ценными, дважды проверяя внутреннее состояние во время выполнения тестов белого ящика, которые вы написали. Включение Debug.Assert во время модульного тестирования имеет важное значение, поскольку вы редко отправляете код с поддержкой DEBUG (если производительность вообще не важна). Код DEBUG запускается только два раза, когда вы либо 1) проводите крошечное тестирование интеграции, которое вы действительно выполняете, за исключением всех благих намерений, и 2) запускаете модульные тесты.

С помощью тестов Debug.Assert легко проверять инварианты по мере их написания. Эти проверки служат проверкой работоспособности при выполнении модульных тестов.

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

Это увеличивает значение модульных тестов.

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

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

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

"Может быть только один", кажется разумным принципом. Это обычная практика? Или вы отключаете свои отладочные утверждения при модульном тестировании, чтобы они могли использоваться для интеграционного тестирования?

Избыточность не вредит никому, кроме времени выполнения ваших юнит-тестов. Если у вас действительно есть 100% охват, время выполнения может быть проблемой. В противном случае нет, я категорически не согласен. Нет ничего плохого в том, чтобы автоматически проверять свое предположение в середине теста. Это практически определение "тестирования".

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

Обычно целью модульного тестирования не является тестирование поведения вашего кода в случае нарушения инвариантных предположений. Другими словами, если написанная вами документация гласит: «если вы передадите null в качестве параметра, результаты не определены», вам не нужно проверять, что результаты действительно непредсказуемы. Если результаты сбоя четко определены, они не являются неопределенными, и 1) это не должен быть Debug.Assert, 2) вы должны точно определить, каковы результаты, и 3) проверить этот результат. Если вам нужно провести модульное тестирование качества ваших внутренних отладочных утверждений, то 1) подход Эндрю Гранта по созданию платформ Assertion в качестве тестируемого актива, вероятно, следует проверить как ответ, и 2) вау, у вас есть потрясающее тестовое покрытие! И я думаю, что это в значительной степени личное решение, основанное на требованиях проекта. Но я все еще думаю, что утверждения отладки важны и ценны.

Другими словами: Debug.Assert () значительно увеличивает значение модульных тестов, а избыточность является функцией.

0 голосов
/ 20 июля 2017

Прошло много времени с тех пор, как этот вопрос был задан, но я думаю, что у меня есть другой способ проверки вызовов Debug.Assert () из модульного теста с использованием кода C #. Обратите внимание на блок #if DEBUG ... #endif, который необходим для пропуска теста, когда он не выполняется в конфигурации отладки (в этом случае Debug.Assert () не будет запущен в любом случае).

[TestClass]
[ExcludeFromCodeCoverage]
public class Test
{
    #region Variables              |

    private UnitTestTraceListener _traceListener;
    private TraceListenerCollection _originalTraceListeners;

    #endregion

    #region TestInitialize         |

    [TestInitialize]
    public void TestInitialize() {
        // Save and clear original trace listeners, add custom unit test trace listener.
        _traceListener = new UnitTestTraceListener();
        _originalTraceListeners = Trace.Listeners;
        Trace.Listeners.Clear();
        Trace.Listeners.Add(_traceListener);

        // ... Further test setup
    }

    #endregion
    #region TestCleanup            |

    [TestCleanup]
    public void TestCleanup() {
        Trace.Listeners.Clear();
        Trace.Listeners.AddRange(_originalTraceListeners);
    }

    #endregion

    [TestMethod]
    public void TheTestItself() {
        // Arrange
        // ...

        // Act
        // ...
        Debug.Assert(false, "Assert failed");



    // Assert

#if DEBUG        
    // NOTE This syntax comes with using the FluentAssertions NuGet package.
    _traceListener.GetWriteLines().Should().HaveCount(1).And.Contain("Fail: Assert failed");
#endif

    }
}

Класс UnitTestTraceListener выглядит следующим образом:

[ExcludeFromCodeCoverage]
public class UnitTestTraceListener : TraceListener
{
    private readonly List<string> _writes = new List<string>();
    private readonly List<string> _writeLines = new List<string>();

    // Override methods
    public override void Write(string message)
    {
        _writes.Add(message);
    }

    public override void WriteLine(string message)
    {
        _writeLines.Add(message);
    }

    // Public methods
    public IEnumerable<string> GetWrites()
    {
        return _writes.AsReadOnly();
    }

    public IEnumerable<string> GetWriteLines()
    {
        return _writeLines.AsReadOnly();
    }

    public void Clear()
    {
        _writes.Clear();
        _writeLines.Clear();
    }
}
...