Это отрывок из электронной книги «Единичное тестирование » Марка Клифтона, любезно предоставленный Syncfusion.
В этой статье мы начнем говорить о некоторых продвинутых темах, связанных с модульным тестированием. Это включает такие вещи, как циклометрическая сложность, методы, поля и рефлексия.
Циклометрическая сложность
Циклометрическая сложность — это мера количества независимых путей в вашем коде. Тестирование покрытия кода предназначено для обеспечения того, чтобы ваши тесты выполняли все возможные пути кода. Тестирование покрытия кода доступно в версиях Visual Studio Test, Premium и Ultimate. Покрытие кода не является частью NUnit. Также доступно стороннее решение NCover .
Чтобы проиллюстрировать эту проблему, рассмотрим этот код, который анализирует параметры командной строки:
открытый статический класс CommandLineParser { /// /// Возвращает список опций, основанных на парсинге /// параметров командной строки. Функция возвращает /// указанные опции и параметр option, если требуется. /// Открытый статический словарь <строка, строка> Parse (строка cmdLine) { Словарь <строка, строка> параметры = новый Словарь <строка, строка> (); string [] items = cmdLine.Split (''); int n = 0; while (n <items.Length) { строковый параметр = items [n]; string param = String.Empty; if (опция [0]! = '-') { бросить новое ApplicationException («Ожидается опция.»); } if (option == "-f") { // Параметр был предоставлен? if (items.Length <= n + 1) { бросить новое ApplicationException ("Имя файла не указано."); } param = items [n + 1]; // Это параметр или другая опция? if (param [0] == '-') { бросить новое ApplicationException ("Имя файла не указано."); } ++ п; // Пропустить параметр имени файла. } options [option] = param; ++ п; } варианты возврата; } }
и пара простых тестов (обратите внимание, что в этих тестах пропускаются пути кода, которые приводят к созданию исключений):
[TestFixture] открытый класс CodeCoverageTests { [Тестовое задание] public void CommandParserTest () { Словарь <string, string> options = CommandLineParser.Parse ("- a -b"); Assert.That (options.Count == 2, «Ожидается, что счет будет 2»); Assert.That (options.ContainsKey ("- a"), "Ожидаемая опция '-a'"); Assert.That (options.ContainsKey ("- b"), "Ожидаемая опция '-b'"); } [Тестовое задание] public void FilenameParsingTest () { Словарь <string, string> options = CommandLineParser.Parse ("- f foobar"); Assert.That (options.Count == 1, «Ожидается, что счет будет 1»); Assert.That (options.ContainsKey ("- f"), "Ожидаемая опция '-f'"); Assert.That (options ["- f"] == "foobar"); } }
Теперь давайте посмотрим, как может выглядеть тест покрытия кода, сначала написав помощник по покрытию кода для бедного человека:
открытый статический класс { public static List CoveragePoints {get; устанавливать;} общедоступная статическая пустота Reset () { CoveragePoints = новый список (); } [Условное ( "DEBUG")] общедоступная статическая пустота { CoveragePoints.Add (coveragePoint); } }
Нам также понадобится простой метод расширения; Вы поймете, почему через минуту:
открытый статический класс ListExtensions { public static bool HasOrderedItems (этот элемент ListList, int [] items) { int n = 0; foreach (int i в itemList) { if (i! = items [n]) { вернуть ложь; } ++ п; } вернуть истину; } }
Теперь мы можем добавить контрольные точки покрытия в наш код, который будет скомпилирован в приложение, когда оно будет скомпилировано в режиме отладки (жирные красные линии добавлены):
открытый статический класс CommandLineParser { /// /// Возвращает список опций, основанных на парсинге /// параметров командной строки. Функция возвращает /// указанные опции и параметр option, если требуется. /// Открытый статический словарь <строка, строка> Parse (строка cmdLine) { Словарь <строка, строка> параметры = новый Словарь <строка, строка> (); string [] items = cmdLine.Split (''); int n = 0; while (n <items.Length) { Coverage.Set (1); // МЫ ДОБАВЛЯЕМ ЭТУ НАСТРОЙКУ ПОКРЫТИЯ строковый параметр = items [n]; string param = String.Empty; if (опция [0]! = '-') { бросить новое ApplicationException («Ожидается опция.»); } if (option == "-f") { Coverage.Set (2); // МЫ ДОБАВЛЯЕМ ЭТУ НАСТРОЙКУ ПОКРЫТИЯ // Параметр был предоставлен? if (items.Length <= n + 1) { бросить новое ApplicationException ("Имя файла не указано."); } param = items [n + 1]; // Это параметр или другая опция? if (param [0] == '-') { бросить новое ApplicationException ("Имя файла не указано."); } ++ п; // Пропустить параметр имени файла. } options [option] = param; ++ п; } варианты возврата; } }
И теперь мы можем написать следующий тестовый прибор:
[TestFixture] открытый класс CommandParserCoverageTests { [Настроить] public void CoverageSetup () { Coverage.Reset (); } [Тестовое задание] public void CommandParserTest () { Словарь <string, string> options = CommandLineParser.Parse ("- a -b"); Assert.That (Coverage.CoveragePoints.HasOrderedItems (new [] {1, 1})); } [Тестовое задание] public void FilenameParsingTest () { Словарь <string, string> options = CommandLineParser.Parse ("- f foobar"); Assert.That (Coverage.CoveragePoints.HasOrderedItems (new [] {1, 2})); } }
Обратите внимание, что теперь мы игнорируем фактические результаты, но гарантируем, что требуемые блоки кода выполняются.
Тестирование белого ящика: проверка защищенных и закрытых полей и методов
Возможно, модульный тест должен касаться только открытых полей и методов. Контраргументом этого является то, что для тестирования всей реализации требуется доступ к защищенным или закрытым полям, утверждение их состояния и возможность модульного тестирования защищенных или закрытых методов. Учитывая, что, вероятно, нежелательно показывать большинство низкоуровневых вычислений, и именно эти методы нужно тестировать, наиболее вероятно, что необходимо тестирование хотя бы защищенных или частных методов. Есть несколько доступных вариантов.
Экспонирование методов и полей в тестовом режиме
Этот пример иллюстрирует концепцию:
публичный класс { #if ТЕСТ общественности #else частный #endif void SomeComputation () { } }
Несмотря на то, что это выполнимо, он производит ужасный исходный код и подвергает серьезному риску, что кто-то может фактически вызвать метод с определенным символом TEST, только чтобы обнаружить, что его или ее код ломается в производственной сборке, где символ TEST не определен.
Получение тестового класса
В качестве альтернативы, если методы защищены, рассмотрите возможность создания тестового класса:
открытый класс DoesSomethingElse { protected void SomeComputation () {} } Открытый класс DoesSomethingElseTesting: DoesSomethingElse { public void TestSomeComputation () { base.SomeComputation (); } }
Это позволяет создать экземпляр производного класса тестирования и получить доступ к защищенному методу с помощью открытого метода в подклассе.
отражение
Наконец, можно использовать отражение для закрытых методов или закрытых классов. Следующее иллюстрирует закрытый метод и его выполнение посредством отражения в тесте:
публичный класс { private void SomeComputation () {} } [TestClass] открытый класс DoesSomethingTest { [Метод испытания] public void SomeComputationTest () { DoSomething ds = new DoesSomething (); Тип t = ds.GetType (); MethodInfo mi = t.GetMethod ("SomeComputation", BindingFlags.Instance | BindingFlags.NonPublic); mi.Invoke (ds, null); } }