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
заменить на генерацию исключения.
В очередной раз я понял, что детали имеют большое значение, когда речь заходит о программировании руками. Какой бы блестящей ни была теоретическая подготовка, разработка живого кода никогда не будет лишней.