Лучшая практика для отладки.

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

Изменить: я обновил "Assert" для отладки assert, чтобы отличить утверждение в тестируемом коде от строк в unit test, которое проверяет состояние после запуска теста.

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

Ответ 1

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

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

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

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

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

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

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

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

Теперь как мы обрабатываем ваши модульные тесты?

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

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

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

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

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

UPDATE:

Вот пример TraceListener, который можно использовать для утверждений unit test. Вы должны добавить экземпляр в коллекцию 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();
    }
}

Вы можете добавить Assertions в коллекцию 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.
}

Ответ 2

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

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

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

Второй момент - это то, что, похоже, влечет за собой несколько разработчиков. unit test черта из всех вещей, которые вы создали для вашего кода, а также для Assert или throw исключений для всего остального. В конце концов, если ваш код НЕ собирается обрабатывать эти ситуации, и вы вызываете их, что вы ожидаете, что произойдет?
Вы знаете те части документации на C/С++, которые говорят о "undefined поведении"? Это оно. Залог и поручительство трудно.

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

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

Обратите внимание: причина "debug" -asserts - это компромисс между усердием/безопасностью и быстрым/малым.

Ответ 6

Хорошая установка unit test будет иметь возможность ловить утверждения. Если инициируется assert, текущий тест должен завершиться неудачно, а следующий будет запущен.

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

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

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

Ответ 9

Как и все упомянутые, операторы Debug.Assert всегда должны быть true, даже если аргументы неверны, утверждение должно быть правдой, чтобы приложение не попало в недопустимое состояние и т.д.

Debug.Assert(_counter == somethingElse, "Erk! Out of wack!");

Вы не сможете проверить это (и, вероятно, не хотите, потому что вы ничего не можете сделать действительно!)

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

if (param1 == null)
  throw new ArgumentNullException("param1", "message to user")

Такое "утверждение" в вашем коде все еще очень проверяемо.

PK: -)

Ответ 10

Прошло некоторое время с тех пор, как был задан этот вопрос, но я думаю, что у меня есть другой способ проверки вызовов Debug.Assert() из unit test с использованием кода С#. Обратите внимание на блок #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();
    }
}