1 : C
Дипломная работа
19 янв 2023
20 страниц

Готовая работа по технико-экономической оптимизации

3. Модульное тестирование
3.1. Теоретическое вступление
3.1.1. Задачи и цели модульного тестирования
Каждая сложная программная система состоит из отдельных частей – модулей, выполняющих ту или иную функцию в составе системы. Для того, чтобы удостовериться в корректной работе системы в целом, необходимо вначале протестировать каждый модуль системы в отдельности. В случае возникновения проблем это позволит проще выявить модули, вызвавшие проблему, и устранить соответствующие дефекты в них. Такое тестирование модулей по отдельности получило называние модульного тестирования (unit testing).
Для каждого модуля, подвергаемого тестированию, разрабатывается тестовое окружение, включающее в себя драйвер и заглушки, готовятся тест-требования и тест-планы, описывающие конкретные тестовые примеры.
Основная цель модульного тестирования – удостовериться в соответствии требованиям каждого отдельного модуля системы перед тем, как будет произведена его интеграция в состав системы.
При этом в ходе модульного тестирования решаются четыре основные задачи.
1. Поиск и документирование несоответствий требованиям – это классическая задача тестирования, включающая в себя не только разработку тестового окружения и тестовых примеров, но и выполнение тестов, протоколирование результатов выполнения, составление отчетов о проблемах.
2. Поддержка разработки и рефакторинга низкоуровневой архитектуры системы и межмодульного взаимодействия – эта задача больше свойственна "легким" методологиям типа XP, где применяется принцип тестирования перед разработкой (Test-driven development), при котором основным источником требований для программного модуля является тест, написанный до самого модуля. Однако, даже при классической схеме тестирования модульные тесты могут выявить проблемы в дизайне системы и нелогичные или запутанные механизмы работы с модулем.
3. Поддержка рефакторинга модулей – эта задача связана с поддержкой процесса изменения системы. Достаточно часто в ходе разработки требуется проводить рефакторинг модулей или их групп – оптимизацию или полную переделку программного кода с целью повышения его сопровождаемости, скорости работы или надежности. Модульные тесты при этом являются мощным инструментом для проверки того, что новый вариант программного кода работает в точности так же, как и старый.
4. Поддержка устранения дефектов и отладки — эта задача сопряжена с обратной связью, которую получают разработчики от тестировщиков в виде отчетов о проблемах. Подробные отчеты о проблемах, составленные на этапе модульного тестирования, позволяют локализовать и устранить многие дефекты в программной системе на ранних стадиях ее разработки или разработки ее новой функциональности.
В силу того, что модули, подвергаемые тестированию, обычно невелики по размеру, модульное тестирование считается наиболее простым (хотя и достаточно трудоемким) этапом тестирования системы. Однако, несмотря на внешнюю простоту, с модульным тестированием связано две проблемы.
1. Не существует единых принципов определения того, что в точности является отдельным модулем.
2. Различия в трактовке самого понятия модульного тестирования – понимается ли под ним обособленное тестирование модуля, работа которого поддерживается только тестовым окружением, или речь идет о проверке корректности работы модуля в составе уже разработанной системы. В последнее время термин "модульное тестирование" чаще используется во втором смысле, хотя в этом случае речь скорее идет об интеграционном тестировании.
Эти две проблемы рассмотрены в двух следующих разделах.
3.1.2. Понятие модуля и его границ. Тестирование классов
Традиционное определение модуля с точки зрения его тестирования: "модуль – это компонент минимального размера, который может быть независимо протестирован в ходе верификации программной системы". В реальности часто возникают проблемы с тем, что считать модулем. Существует несколько подходов к данному вопросу:
• модуль – это часть программного кода, выполняющая одну функцию с точки зрения функциональных требований;
• модуль – это программный модуль, т.е. минимальный компилируемый элемент программной системы;
• модуль – это задача в списке задач проекта (с точки зрения его менеджера);
• модуль – это участок кода, который может уместиться на одном экране или одном листе бумаги;
• модуль – это один класс или их множество с единым интерфейсом.
Обычно за тестируемый модуль принимается либо программный модуль (единица компиляции) в случае, если система разрабатывается на процедурном языке программирования, либо класс, если система разрабатывается на объектно-ориентированном языке.
В случае систем, написанных на процедурных языках, процесс тестирования модуля происходит так – для каждого модуля разрабатывается тестовый драйвер, вызывающий функции модуля и собирающий результаты их работы, и набор заглушек, которые имитируют поведение функций, содержащихся в других модулях и не попадающих под тестирование данного модуля. При тестировании объектно-ориентированных систем существует ряд особенностей, прежде всего вызванных инкапсуляцией данных и методов в классах.
В случае объектно-ориентированных систем более мелкое деление классов и использование отдельных методов в качестве тестируемых модулей нецелесообразно в связи с тем, что для тестирования каждого метода потребуется разработка тестового окружения, сравнимого по сложности с уже написанным программным кодом класса. Кроме того, декомпозиция класса нарушает принцип инкапсуляции, согласно которому объекты каждого класса должны вести себя как единое целое с точки зрения других объектов.
Процесс тестирования классов как модулей иногда называют компонентным тестированием. В ходе такого тестирование проверяется взаимодействие методов внутри класса и правильность доступа методов к внутренним данным класса. При таком тестировании возможно обнаружение не только стандартных дефектов, связанных с выходами за границы диапазона или неверно реализованными требованиями, а также обнаружение специфических дефектов объектно-ориентированного программного обеспечения:
• дефектов инкапсуляции, в результате которых, например, сокрытые данные класса оказывается недоступными при помощи соответствующих публичных методов;
• дефектов наследования, при наличии которых схема наследования блокирует важные данные или методы от классов-потомков;
• дефектов полиморфизма, при которых полиморфное поведение класса оказывается распространенным не на все возможные классы;
• дефектов инстанцирования, при которых во вновь создаваемых объектах класса не устанавливаются корректные значения по умолчанию параметров и внутренних данных класса.
Однако, выбор класса в качестве тестируемого модуля имеет и ряд сопряженных проблем.
Определение степени полноты тестирования класса. В том случае, если в качестве тестируемого модуля выбран класс, не совсем ясно, как определять степень полноты его тестирования. С одной стороны, можно использовать классический критерий полноты покрытия программного кода тестами: если полностью выполнены все структурные элементы всех методов, как публичных, так и скрытых, — то тесты можно считать полными.
Однако существует альтернативный подход к тестированию класса, согласно которому все публичные методы должны предоставлять пользователю данного класса согласованную схему работы и достаточно проверить типичные корректные и некорректные сценарии работы с данным классом. Т.е., например, в классе, объекты которого представляют записи в телефонной книжке, одним из типичных сценариев работы будет "Создать запись искать запись и найти ее удалить запись искать запись вторично и получить сообщение об ошибке".
Различия в этих двух методах напоминают различия между тестированием "черного" и "белого" ящиков, но на самом деле второй подход отличается от "черного ящика" тем, что функциональные требования к системе могут быть составлены на уровне более высоком, чем отдельные классы, и установление адекватности тестовых сценариев требованиям остается на откуп тестировщику.
Протоколирование состояний объектов и их изменений. Некоторые методы класса предназначены не для выдачи информации пользователю, а для изменения внутренних данных объекта класса. Значение внутренних данных объекта определяет его состояние в каждый отдельный момент времени, а вызов методов, изменяющих данные, изменяет и состояние объекта. При тестировании классов необходимо проверять, что класс адекватно реагирует на внешние вызовы в любом из состояний. Однако, зачастую из-за инкапсуляции данных невозможно определить внутреннее состояние класса программными способами внутри драйвера.
В этом случае может помочь составление схемы поведения объекта как конечного автомата с определенным набором состояний (подобно тому, как это было описано в теме 2 в разделе "Генераторы сигналов. Событийно-управляемый код"). Такая схема может входить в низкоуровневую проектную документацию (например, в составе описания архитектуры системы), а может составляться тестировщиком или разработчиком на основе функциональных требований к системе. В последнем случае для определения всех возможных состояний может потребоваться ручной анализ программного кода и определение его соответствия требованиям. Автоматизированное тестирование в этом случае может лишь определить, по всем ли выявленным состояниям осуществлялись переходы и все ли возможные реакции проверялись.
Тестирование изменений. Как уже упоминалось выше, модульные тесты – мощный инструмент проверки корректности изменений, внесенных в исходный код при рефакторинге. Однако, в результате рефакторинга только одного класса, как правило, не меняется его внешний интерфейс с другими классами (интерфейсы меняются при рефакторинге сразу нескольких классов). В результате обычных эволюционных изменений системы у класса может меняться внешний интерфейс, причем как по формальным (изменяются имена и состав методов, их параметры), так и по функциональным признакам (при сохранении внешнего интерфейса меняется логика работы методов). Для проведения модульного тестирования класса после таких изменений потребуется изменение драйвера и, возможно, заглушек. Но только модульного тестирования в данном случае недостаточно, необходимо также проводить и интеграционное тестирование данного класса вместе со всеми классами, которые связаны с ним по данным или по управлению.
Вне зависимости от того, на какие модули, подвергаемые тестированию, разбивается система, рекомендуется изложить принципы выделения тестируемых модулей в плане и стратегии тестирования, а также составить на базе структурной схемы архитектуры системы новую структурную схему, на которой отметить все тестируемые модули. Это позволит спрогнозировать состав и сложность драйверов и заглушек, требуемых для модульного тестирования системы. Такая схема также может использоваться позже на этапе модульного тестирования для выделения укрупненных групп модулей, подвергаемых интеграции.
3.1.3. Подходы к проектированию тестового окружения
Вне зависимости от того, какая минимальная единица исходных кодов системы выбирается за минимальный тестируемый модуль, существует еще одно различие в подходах к модульному тестированию.
Первый подход к модульному тестированию основывается на предположении, что функциональность каждого вновь разработанного модуля должна проверяться в автономном режиме без его интеграции с системой. Здесь для каждого вновь разрабатываемого модуля создается тестовый драйвер и заглушки, при помощи которых выполняется набор тестов. Только после устранения всех дефектов в автономном режиме производится интеграция модуля в систему и проводится тестирование на следующем уровне. Достоинством данного подхода является более простая локализация ошибок в модуле, поскольку при автономном тестировании исключается влияние остальных частей системы, которое может вызывать маскировку дефектов (эффект четного числа ошибок). Основной недостаток данного метода – повышенная трудоемкость написания драйверов и заглушек, поскольку заглушки должны адекватно моделировать поведение системы в различных ситуациях, а драйвер должен не только создавать тестовое окружение, но и имитировать внутреннее состояние системы, в составе которой должен функционировать модуль.
Второй подход построен на предположении, что модуль все равно работает в составе системы и если модули интегрировать в систему по одному, то можно протестировать поведение модуля в составе всей системы. Этот подход свойственен большинству современных "облегченных" методологий разработки, в том числе и XP.
В результате применения такого подхода резко сокращаются трудозатраты на разработку заглушек и драйверов – в роли заглушек выступает уже оттестированная часть системы, а драйвер выполняет только функции передачи и приема данных, не моделируя внутреннее состояние системы.
Тем не менее, при использовании данного метода возрастает сложность написания тестовых примеров – для приведения в нужное состояние системы заглушек, как правило, требуется только установить значения тестовых переменных, а для приведения в нужное состояние части реальной системы необходимо выполнить целый сценарий. Каждый тестовый пример в этом случае должен содержать такой сценарий.
Кроме того, при этом подходе не всегда удается локализовать ошибки, скрытые внутри модуля, которые могут проявиться при интеграции следующих модулей.
3.2. На примере "Калькулятора"
Рассмотренный на предыдущем семинаре пример прост прежде всего за счет того, что нам не приходится создавать тестового окружения. Чтобы увидеть весь описанный механизм в действии, протестируем метод RunEstimate класса AnalaizerClass. Этот метод использует методы из класса CalcClass, в надежности которых мы не уверены. Заменим эти методы заглушкой, состоящей исключительно из функций стандартного класса Math. Для этого воспользуемся файлом My.dll и добавим его в проект.
Продемонстрируем, как создать тестовое окружение и запустить метод. Проверяем операцию сложения на примере 2+2, т.е. в стеке до начала выполнения самой операции (т.е. после компиляции) находятся следующие элементы: " 2 ", " 2 ", " + ".
private void buttonStart_Click(object sender, EventArgs e)
{
// создаем провайдер для генерирования и компиляции кода на C#
System.CodeDom.Compiler.CodeDomProvider prov =
System.CodeDom.Compiler.CodeDomProvider.CreateProvider("CSharp");
// создаем параметры компилирования
System.CodeDom.Compiler.CompilerParameters cmpparam = new
System.CodeDom.Compiler.CompilerParameters();
// результат компиляции - библиотека
cmpparam.GenerateExecutable = false;
// не включаем информацию отладчика
cmpparam.IncludeDebugInformation = false;
// подключаем 2-е стандартные библиотеки и библиотеку
CalcClass.dll
cmpparam.ReferencedAssemblies.Add(Application.StartupPath +
"\\CalcClass.dll");
cmpparam.ReferencedAssemblies.Add("System.dll");
cmpparam.ReferencedAssemblies.Add("System.Windows.Forms.dll");
// имя выходной сборки - My.dll
cmpparam.OutputAssembly = "My.dll";
// компилируем класс AnalaizerClass с заданными параметрами
System.CodeDom.Compiler.CompilerResults res =
prov.CompileAssemblyFromFile(cmpparam, Application.StartupPath + "\\AnalaizerClass.cs");
// Выводим результат компилирования на экран
if (res.Errors.Count != 0)
{
richTextBox1.Text += res.Errors[0].ToString();
}
else
{
// загружаем только что скомпилированную сборку(здесь тонкий
момент - если мы прото загрузим сборку из файла, то он будет заблокирован,
// acces denied, поэтому вначале читаем его в поток и лишь
потом подключаем)
System.IO.BinaryReader reader = new
System.IO.BinaryReader(new System.IO.FileStream(Application.StartupPath + "\\My.dll", System.IO.FileMode.Open, System.IO.FileAccess.Read));
Byte[] asmBytes = new Byte[reader.BaseStream.Length];
reader.Read(asmBytes, 0, (Int32) reader.BaseStream.Length);
reader.Close();
reader = null;
System.Reflection.Assembly assm =
System.Reflection.Assembly.Load(asmBytes);
Type[] types = assm.GetTypes();
Type analaizer = types[0];
// находим метод CheckCurrency - к счастью, он единственный
System.Reflection.MethodInfo addinfo =
analaizer.GetMethod("RunEstimate");
System.Reflection.FieldInfo fieldopz =
analaizer.GetField("opz");
System.Collections.ArrayList ar = new
System.Collections.ArrayList();
ar.Add("2");
ar.Add("2");
ar.Add("+");
fieldopz.SetValue(null, ar);
richTextBox1.Text += addinfo.Invoke(null, null).ToString();
asmBytes = null;
}
prov.Dispose();
}
9.1.
Замечание. На самом деле данный подход позволяет выявить множество недостатков программы, которые другими тестами не выявляются. Можно попробовать поэкспериментировать с "Калькулятором" и убедиться, что он работает корректно. Однако, если в тестируемый метод подать на вход не " 2 ", "2 ", " + ", а " 2 ", " 2 ", " + ", " + ", то программа закончит работу с исключением. Это говорит о том, что метод RunEstimate написан не корректно. Можно, например, было бы скрыть, т. е. сделать доступ private, всем методам AnalaizerClass, кроме Estimate (это было бы более правильно, но для простоты тестирования они сделаны public. Стоит отметить, что Visual Studio имеет также механизмы для тестирования подобных методов.). Тем самым мы не позволим другим выполнять "потенциально опасные" методы и передавать им некорректные значения. Однако это не является достаточным механизмом защиты программы. Необходимо провести более качественную валидацию используемых методами параметров.
Замечание. К проблеме создания тестового окружения можно подойти с двух сторон – либо откомпилировать код, с заранее подключенными dll файлами к проекту, либо воспользоваться областью CodeDom и компилировать в процессе выполнения. Это особенно удобно, если нужно менять тестовое окружение в процессе работы.
3.3. Задание
Составить тестовые требования и провести ручное модульное тестирование следующих методов:
///
/// Проверка корректности скобочной структуры входного выражения
///
/// true - если все нормально,
/// false - если есть ошибка
/// метод бежит по входному выражению, символ за
/// символом анализируя его и считая количество скобок.
/// В случае возникновения
/// ошибки возвращает false, а в erposition записывает позицию,
/// на которой возникла ошибка.
1. public static bool CheckCurrency()
///
/// Форматирует входное выражение, выставляя между
/// операторами пробелы и удаляя лишние, а также отлавливает
/// неопознанные операторы, следит за концом строки
/// а также отлавливает ошибки на конце строки
///
/// конечную строку или сообщение об ошибке,
/// начинающиеся со спец. символа &
2. public static string Format()
///
/// Создает массив, в котором располагаются операторы и
/// символы, представленные в обратной польской записи (безскобочной)
/// На этом же этапе отлавливаются почти все остальные
/// ошибки (см код). По сути - это компиляция.
///
/// массив обратной польской записи
3. public static System.Collections.ArrayList CreateStack()
///
/// Вычисление обратной польской записи
///
/// результат вычислений или сообщение об ошибке
4. public static string RunEstimate()

Vladimir.Burdak Vladimir.Burdak
3500 р