На главную

Debug.Assert и модульные тесты

14-09-2018

На днях делал тестовый проект. В одном месте надо было прочитать из входного потока пары целых чисел, разделённые пробелом. Я для этого написал несложный метод:

public static IEnumerable<(int, int)> ReadIntPairs(TextReader reader)
{
    var line = reader.ReadLine();
    while (line != null)
    {
        var numbers = line.Split(' ');

        yield return (int.Parse(numbers[0]), int.Parse(numbers[1]));

        line = reader.ReadLine();
    }
}

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

Да, не надо. Но для средств автоматической проверки кода и для программистов, которые будут с ним разбираться, мы бы хотели бы оставить подсказку: мы ожидаем, что чисел будет ровно два. В C# такое условие можно оформить с помощью метода Debug.Assert:

public static IEnumerable<(int, int)> ReadIntPairs(TextReader reader)
{
    var line = reader.ReadLine();
    while (line != null)
    {
        var numbers = line.Split(' ');
        Debug.Assert(numbers.Length == 2);

        yield return (int.Parse(numbers[0]), int.Parse(numbers[1]));

        line = reader.ReadLine();
    }
}

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

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

[TestMethod]
public void ReadIntPairs_WithSingleNumberInsteadOfPair_ThrowsException()
{
    using (var reader = new StringReader("3 100\n5\n4 700\n"))
    {
        var intPairs = Program.ReadIntPairs(reader)
                              .ToArray();
    }
}

Обычно в названии метода я пишу, какое исключение будет создано: суффикс тестовых методов имеет вид ThrowsArgumentNullException или ThrowsInvalidOperationException. Но в данном случае я просто не помню, какой тип исключения выбросит Debug.Assert и оставляю название метода неполным. Запущу и всё увижу.

После появления ошибки я надеюсь исправить название теста и вставить атрибут [ExpectedException] с правильным типом исключения.

Однако при запуске перестали выполняться все тесты. Модуль, который их запускает, сам завершился с ошибкой:

Активный тестовый запуск прерван. Причина: Assertion Failed.

На английском:

The active test run was aborted. Reason: Assertion Failed.

Похожее сообщение об ошибке вы увидите, если используете xUnit вместо MSTest. Кажется, что дело в Debug.Assert, но почему? Разве модуль запуска тестов не должен просто перехватить и обработать исключение?

Оказывается, нет.

Это поведение by design. Если вы ожидаете, что на вход вашей функции попадут неправильные значения, явно проверьте их, и выбросьте исключение, унаследованное от ArgumentException. Но если вы ожидаете правильные значения, которые удовлетворяют неочевидным условиям, применяйте Debug.Assert. Это поможет Visual Studio и Resharper не ругать ваш правильный код.

В моём случае, поскольку условия задачи гарантировали правильные данные, я оставил конструкцию Debug.Assert и удалил тест. У вас может получиться наоборот: тест придётся оставить, а Debug.Assert заменить на генерацию исключения.

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