Аннотация: Рассматриваются особенности модульного тестирования, обсуждаются
подходы к тестированию на основе потока управления, потока данных.
Обсуждаются динамические и статические методы при структурном
подходе. Рассматривается пример модульного тестирования.
Рассматривается взаимосвязь сборки модулей и методов интеграционного
тестирования. Обсуждаются подходы монолитного, инкрементального,
нисходящего и восходящего тестирования. Рассматриваются особенности
интеграционного тестирования в процедурном программировании.
Модульное
Модульное тестирование — это тестирование программы на уровне
отдельно взятых модулей, функций или классов. Цель модульного
тестирования состоит в выявлении локализованных в модуле ошибок в
реализации алгоритмов, а также в определении степени готовности
системы к переходу на следующий уровень разработки и тестирования. Модульное тестирование проводится по принципу «белого ящика», то есть
основывается на знании внутренней структуры программы, и часто
включает те или иные методы анализа покрытия кода.
Модульное тестирование обычно подразумевает создание вокруг каждого
модуля определенной среды, включающей заглушки для всех интерфейсов
тестируемого модуля. Некоторые из них могут использоваться для подачи
входных значений, другие для анализа результатов, присутствие третьих
может быть продиктовано требованиями, накладываемыми компилятором и
сборщиком.
На уровне модульного тестирования проще всего обнаружить дефекты,
связанные с алгоритмическими ошибками и ошибками кодирования
алгоритмов, типа работы с условиями и счетчиками циклов, а также с
использованием локальных переменных и ресурсов. Ошибки, связанные с
неверной трактовкой данных, некорректной реализацией интерфейсов,
совместимостью, производительностью и т.п. обычно пропускаются на
уровне модульного тестирования и выявляются на более поздних стадиях
тестирования.
Именно эффективность обнаружения тех или иных типов дефектов должна
определять стратегию модульного тестирования, то есть расстановку
акцентов при определении набора входных значений. У организации,
занимающейся разработкой программного обеспечения, как правило,
имеется историческая база данных ( Repository ) разработок, хранящая
конкретные сведения о разработке предыдущих проектов: о версиях и
сборках кода ( build ) зафиксированных в процессе разработки продукта,
о принятых решениях, допущенных просчетах, ошибках, успехах и т.п.
Проведя анализ характеристик прежних проектов, подобных заказанному
организации, можно предохранить новую разработку от старых ошибок,
например, определив типы дефектов, поиск которых наиболее эффективен
на различных этапах тестирования.
В данном случае анализируется этап модульного тестирования. Если
анализ не дал нужной информации, например, в случае проектов, в
которых соответствующие данные не собирались, то основным правилом
становится поиск локальных дефектов, у которых код, ресурсы и
информация, вовлеченные в дефект, характерны именно для данного
модуля. В этом случае на модульном уровне ошибки, связанные,
например, с неверным порядком или форматом параметров модуля, могут
быть пропущены, поскольку они вовлекают информацию, затрагивающую
другие модули (а именно, спецификацию интерфейса), в то время как
ошибки в алгоритме обработки параметров довольно легко
обнаруживаются.
Являясь по способу исполнения структурным тестированием или
тестированием «белого ящика», модульное тестирование характеризуется
степенью, в которой тесты выполняют или покрывают логику программы
(исходный текст). Тесты, связанные со структурным тестированием,
строятся по следующим принципам:
- На основе анализа потока управления. В этом случае элементы,
которые должны быть покрыты при прохождении тестов, определяются на
основе структурных критериев тестирования С0, С1,С2. К ним относятся
вершины, дуги, пути управляющего графа программы (УГП), условия,
комбинации условий и т. п. - На основе анализа потока данных, когда элементы, которые должны
быть покрыты, определяются при помощи потока данных, т. е.
информационного графа программы.
Тестирование на основе потока управления. Особенности использования
структурных критериев тестирования С0,С1,С2 были рассмотрены в
лекции 3. К ним следует добавить критерий покрытия условий,
заключающийся в покрытии всех логических (булевских) условий в
программе. Критерии покрытия решений (ветвей — С1) и условий не
заменяют друг друга, поэтому на практике используется комбинированный
критерий покрытия условий/решений, совмещающий требования по покрытию
и решений, и условий.
К популярным критериям относятся критерий покрытия функций программы,
согласно которому каждая функция программы должна быть вызвана хотя
бы один раз, и критерий покрытия вызовов, согласно которому каждый
вызов каждой функции в программе должен быть осуществлен хотя бы один
раз. Критерий покрытия вызовов известен также как критерий покрытия
пар вызовов (call pair coverage).
Тестирование на основе потока данных. Этот вид тестирования направлен
на выявление ссылок на неинициализированные переменные и избыточные
присваивания (аномалий потока данных ). Как основа для стратегии
тестирования поток данных впервые был описан в
[
14
]
. Предложенная там
стратегия требовала тестирования всех взаимосвязей, включающих в себя
ссылку (использование) и определение переменной, на которую указывает
ссылка (т. е. требуется покрытие дуг информационного графа
программы). Недостаток стратегии в том, что она не включает критерий
С1, и не гарантирует покрытия решений.
Стратегия требуемых пар
[
15
]
также тестирует упомянутые взаимосвязи.
Использование переменной в предикате дублируется в соответствии с
числом выходов решения, и каждая из таких требуемых взаимосвязей
должна быть протестирована. К популярным критериям принадлежит
критерий СР, заключающийся в покрытии всех таких пар дуг v и w, что
из дуги v достижима дуга w, поскольку именно на дуге может произойти
потеря значения переменной, которая в дальнейшем уже не должна
использоваться. Для «покрытия» еще одного популярного критерия Cdu
достаточно тестировать пары (вершина, дуга), поскольку определение
переменной происходит в вершине УГП, а ее использование — на дугах,
исходящих из решений, или в вычислительных вершинах.
Методы проектирования тестовых путей для достижения заданной степени
тестированности в структурном тестировании. Процесс построения набора
тестов при структурном тестировании принято делить на три фазы:
- Конструирование УГП.
- Выбор тестовых путей.
- Генерация тестов, соответствующих тестовым путям.
Первая фаза соответствует статическому анализу программы, задача
которого состоит в получении графа программы и зависящего от него и
от критерия тестирования множества элементов, которые необходимо
покрыть тестами.
На третьей фазе по известным путям тестирования осуществляется поиск
подходящих тестов, реализующих прохождение этих путей.
Вторая фаза обеспечивает выбор тестовых путей. Выделяют три подхода к
построению тестовых путей:
- Статические методы.
- Динамические методы.
- Методы реализуемых путей.
Статические методы. Самое простое и легко реализуемое решение —
построение каждого пути посредством постепенного его удлинения за
счет добавления дуг, пока не будет достигнута выходная вершина
управляющего графа программы. Эта идея может быть усилена в так
называемых адаптивных методах, которые каждый раз добавляют только
один тестовый путь (входной тест), используя предыдущие пути (тесты)
как руководство для выбора последующих путей в соответствии с
некоторой стратегией. Чаще всего адаптивные стратегии применяются по
отношению к критерию С1. Основной недостаток статических методов
заключается в том, что не учитывается возможная нереализуемость
построенных путей тестирования.
Динамические методы. Такие методы предполагают построение полной
системы тестов, удовлетворяющих заданному критерию, путем
одновременного решения задачи построения покрывающего множества путей
и тестовых данных. При этом можно автоматически учитывать
реализуемость или нереализуемость ранее рассмотренных путей или их
частей. Основной идеей динамических методов является подсоединение к
начальным реализуемым отрезкам путей дальнейших их частей так, чтобы:
1) не терять при этом реализуемости вновь полученных путей; 2)
покрыть требуемые элементы структуры программы.
Методы реализуемых путей. Данная методика
[
16
]
заключается в
выделении из множества путей подмножества всех реализуемых путей.
После чего покрывающее множество путей строится из полученного
подмножества реализуемых путей.
Достоинство статических методов состоит в сравнительно небольшом
количестве необходимых ресурсов, как при использовании, так и при
разработке. Однако их реализация может содержать непредсказуемый
процент брака (нереализуемых путей). Кроме того, в этих системах
переход от покрывающего множества путей к полной системе тестов
пользователь должен осуществить вручную, а эта работа достаточно
трудоемкая. Динамические методы требуют значительно больших ресурсов
как при разработке, так и при эксплуатации, однако увеличение затрат
происходит, в основном, за счет разработки и эксплуатации аппарата
определения реализуемости пути (символический интерпретатор, решатель
неравенств). Достоинство этих методов заключается в том, что их
продукция имеет некоторый качественный уровень — реализуемость путей.
Методы реализуемых путей дают самый лучший результат.
Пример модульного тестирования
Предлагается протестировать класс TCommand, который реализует команду
для склада. Этот класс содержит единственный метод TCommand.GetFullName(), спецификация которого описана (Практикум,
Приложение 2 HLD) следующим образом:
| … | |
| Операция GetFullName() возвращает полное имя команды, соответствующее ее допустимому коду, указанному в поле NameCommand. В противном случае возвращается сообщение «ОШИБКА : Неверный код команды». Операция может быть применена в любой момент. |
|
| … |
Разработаем спецификацию тестового случая для тестирования метода GetFullName на основе приведенной спецификации класса (Табл. 5.1):
| Название класса: TСommand | Название тестового случая: TСommandTest1 |
|
Описание тестового случая: Тест проверяет правильность работы метода GetFullName — получения полного названия команды на основе кода команды. В тесте подаются следующие значения кодов команд (входные значения): -1, 1, 2, 4, 6, 20, (причем -1 — запрещенное значение). |
|
| Начальные условия: Нет. | |
|
Ожидаемый результат: Перечисленным входным значениям должны соответствовать следующие Коду команды -1 должно соответствовать сообщение «ОШИБКА: Неверный Коду команды 1 должно соответствовать полное название команды Коду команды 2 должно соответствовать полное название команды Коду команды 4 должно соответствовать полное название команды Коду команды 6 должно соответствовать полное название команды Коду команды 20 должно соответствовать полное название команды |
Для тестирования метода класса TCommand.GetFullName() был создан
тестовый драйвер — класс TCommandTester. Класс TCommandTester
содержит метод TCommandTest1(), в котором реализована вся
функциональность теста. В данном случае для покрытия спецификации
достаточно перебрать следующие значения кодов команд: -1, 1, 2, 4, 6,
20, (-1 — запрещенное значение) и получить соответствующее им полное
название команды с помощью метода GetFullName() (Пример 5.1 ). Пары
значений (X, Yв) при исполнении теста заносятся в log-файл для
последующей проверки на соответствие спецификации.
После завершения теста следует просмотреть журнал теста, чтобы
сравнить полученные результаты с ожидаемыми, заданными в спецификации
тестового случая TСommandTest1 (Пример 5.2).
class TCommandTester:Tester // Тестовый драйвер
{
...
TCommand OUT;
public TCommandTester()
{
OUT=new TCommand();
Run();
}
private void Run()
{
TCommandTest1();
}
private void TCommandTest1()
{
int[] commands = {-1, 1, 2, 4, 6, 20};
for(int i=0;i<=5;i++)
{
OUT.NameCommand=commands[i];
LogMessage(commands[i].ToString()+
" : "+OUT.GetFullName());
}
}
...
}
Пример
5.1.
Тестовый драйвер
-1 : ОШИБКА : Неверный код команды 1 : ПОЛУЧИТЬ ИЗ ВХОДНОЙ ЯЧЕЙКИ 2 : ОТПРАВИТЬ ИЗ ЯЧЕЙКИ В ВЫХОДНУЮ ЯЧЕЙКУ 4 : ПОЛОЖИТЬ В РЕЗЕРВ 6 : ПРОИЗВЕСТИ ЗАНУЛЕНИЕ 20 : ЗАВЕРШЕНИЕ КОМАНД ВЫДАЧИ
Пример
5.2.
Спецификация классов тестовых случаев
Перевод статьи
«3 Mistakes You’re Probably Making When Unit Testing».

Модульное тестирование это одна из тех вещей, которые разработчики либо любят, либо ненавидят — третьего не дано. Также это одна из тех задач, которые во многих командах поручают «новичку», разработчику-джуниору или любому, кто не занят ничем другим, более важным.
Обычно так происходит, потому что модульное тестирование считается тривиальной задачей, чем-то таким, в чем никто не сумеет ошибиться. Но знаете, что? Таки есть способы напортачить с тестами! В этой статье я расскажу о трех ошибках, которые вы (как и любой другой) можете допустить в ходе тестирования.
1. Тестирование больше одной
вещи за раз
Это распространенная ошибка, которую
допускают джуниоры, когда вы посылаете
их протестировать большие куски кода.
Обычно они находят большие функции и
пытаются протестировать их сверху
донизу. А поскольку эти функции большие,
джуниоры имеют тенденцию использовать
один (или, может, два) test case для каждой,
и в результате получают сценарии
тестирования, выискивающие что-то совсем
ненужное.
Позвольте спросить: кто в этом виноват?
Ваш разработчик, занимающийся тестами?
Или тот, кто написал функцию на сотни
строк?
Модульные тесты подразумевают фокус
на отдельных модулях вашего кода. Если
функция, которую вам нужно протестировать,
не позволяет это сделать, нужно не тесты
менять — это неверный подход. Вместо
этого уделите время рефакторингу кода,
разбейте его на более поддерживаемые
модули, а затем разделите и test cases, чтобы
точно иметь возможность тестировать
все части по отдельности.
Для примера возьмем следующую функцию:
function sayHi(first_name, last_name) {
if(!first_name) return false;
if(!last_name) return false;
[fn_fl, ...fn_tail] = first_name.split("");
first_name = [fn_fl.toUppercase(), ...fn_tail].join("");
[ln_fl, ...ln_tail] = last_name.split("");
last_name = [ln_fl.toUppercase(), ...ln_tail].join("");
console.log("Hello there " + first_name + " " + last_name)
return true
}
Функция в полном порядке, но если вы
попробуете проверить ее при помощи
модульного тестирования, вы столкнетесь
с трудностями. Вам будет сложно изолировать
части ее поведения. Просто протестировать
валидацию или трансформацию переменных
не так легко. Вместо этого можно сделать
рефакторинг и разбить вашу функцию на
более мелкие:
function validateData(fn, ln) {
if(!fn) return false
if(!ln) return false
return true
}
function transform(string) {
[fl, ...tail] = string.split("");
return [fl.toUppercase(), ...tail].join("");
}
function sayHi2(first_name, last_name) {
if(!validateData(first_name, last_name)) {
return false;
}
first_name = transform(first_name)
last_name = transform(last_name)
console.log("Hello there " + first_name + " " + last_name)
return true
}
Благодаря этому вы сможете протестировать
различные части предыдущего поведения.
2. Принятие в расчет внешних
сервисов
Чаще всего код пишется не для того,
чтобы работать изолированно. По факту,
скорее всего он будет работать со
внешними сервисами. Под словом «сервисы»
здесь следует понимать что угодно, от
запроса к Google Map’s API до сохранения файла
на вашем локальном жестком диске.
То есть, ваш код будет взаимодействовать
с чем-то вне среды его выполнения, но
ваши модульные тесты не должны это
учитывать. Почему? Просто потому, что в
ходе выполнения тестов вы не отвечаете
(или не должны отвечать) за статус
сторонних сервисов. Другими словами,
если Google внезапно решит отключить свой
Maps API на несколько часов, почему в таком
случае ваши модульные тесты должны
провалиться?
Если ваши тесты полагаются на
взаимодействие со внешними сервисами,
вы тестируете не только свой код, но и
надежность этих сервисов, а это не должно
быть вашей целью. Внешние сервисы это
совершенно отдельные продукты, они
тестируются в ходе своего цикла
разработки, вам-то зачем сейчас о них
беспокоиться?
Возможно, вы думаете: «Так я же тестирую
код, связанный с этими сервисами!» Ну,
если только вы не пишете драйвера для
них, это сторонние библиотеки, которые
тестируются их создателями.
В общем, что я хочу сказать: фокусируйтесь
на собственном коде. Если код взаимодействует
с базой данных, сделайте заглушку (mock),
если посылаете запрос к какому-нибудь
API, сделайте макеты ответов. В общем, вы
поняли суть.

3. Цель — покрытие тестами
Наконец, еще одна классическая ошибка
это написание тестов ради покрытия
тестами. Сколько раз вам доводилось
слышать призвыв «Пиши тесты, пока не
покроешь ими 90% своего кода»?
Покрытие это лишь один из показателей
для ваших тестов, так же как количество
строк кода — показатель сложности
функций. Но, я уверен, вам встречался
очень краткий код, совершенно непригодный
для чтения.
Не нужно рассматривать покрытие как
способ определить, сколько кода покрыто
тестами. Этот показатель следует
использовать для понимания того, сколько
кода тестами НЕ покрыто, а также для
понимания того, нужно ли его покрывать.
Если ищете показатель для определения
успешности вашей работы, лучше обратите
внимание на точность. То есть, вместо
того чтобы следить за тем, все ли части
кода покрыты тестами, убедитесь, что вы
проверили каждый возможный путь во всех
его вариациях. Что я имею в виду?
Представьте, что у вас есть совершенно
реальная и полезная функция:
function div(a, b) {
if(a > 0) {
console.log("Debug: a is a positive number")
}
return a / b;
}
Проверив, что div(1,1) возвращает 1, а
console.log вызывается, вы можете достигнуть
100% покрытия тестами. Это прекрасно,
однако я уверен, что вы видите, насколько
нерелевантными окажутся эти тесты в ту
минуту, когда вы в качестве второго
аргумента функции передадите 0.
Будет гораздо лучше, если вы добавите
несколько тестов, проверяющих, что все
работает с различными значениями a и b,
пускай даже и не сделаете тестов для
контента вашего if-блока.
Помните, что концентрироваться нужно
на функциональности!
Заключение
Я описал три ошибки, часто допускаемые разработчиками в ходе модульного тестирования (естественно, сам я их тоже допускал не раз). Знаете и другие варианты ошибок при написании тестов? Поделитесь в комментариях!

С этими терминами часто происходит путаница. Если ссылаться на глоссарий ISTQB, то все они — синонимы:
- Модуль, юнит (module, unit): См. компонент.
- Модульное, юнит тестирование (module testing, unit testing): См. компонентное тестирование.
- Компонент (component): Наименьший элемент программного обеспечения, который может быть протестирован отдельно.
- Компонентное тестирование (component testing): Тестирование отдельных компонентов программного обеспечения (IEEE 610).
Тем не менее, некоторые источники описывают ситуацию несколько иначе и я решил выписать другую точку зрения.
Модульное тестирование (оно же юнит-тестирование) используется для тестирования какого-либо одного логически выделенного и изолированного элемента системы (отдельные методы класса или простая функция, subprograms, subroutines, классы или процедуры) в коде. Очевидно, что это тестирование методом белого ящика и чаще всего оно проводится самими разработчиками. Целью тестирования модуля является не демонстрация правильного функционирования модуля, а демонстрация наличия ошибки в модуле, а также в определении степени готовности системы к переходу на следующий уровень разработки и тестирования. На уровне модульного тестирования проще всего обнаружить дефекты, связанные с алгоритмическими ошибками и ошибками кодирования алгоритмов, типа работы с условиями и счетчиками циклов, а также с использованием локальных переменных и ресурсов. Ошибки, связанные с неверной трактовкой данных, некорректной реализацией интерфейсов, совместимостью, производительностью и т.п. обычно пропускаются на уровне модульного тестирования и выявляются на более поздних стадиях тестирования. Изоляция тестируемого блока достигается с помощью заглушек (stubs), манекенов (dummies) и макетов (mockups).
Компонентное тестирование — тип тестирования ПО, при котором тестирование выполняется для каждого отдельного компонента отдельно, без интеграции с другими компонентами. Его также называют модульным тестированием (Module testing), если рассматривать его с точки зрения архитектуры. Как правило, любое программное обеспечение в целом состоит из нескольких компонентов. Тестирование на уровне компонентов (Component Level testing) имеет дело с тестированием этих компонентов индивидуально. Это один из самых частых типов тестирования черного ящика, который проводится командой QA. Для каждого из этих компонентов будет определен сценарий тестирования, который затем будет приведен к Test case высокого уровня -> детальным Test case низкого уровня с предварительными условиями.
Исходя из глубины уровней тестирования, компонентное тестирование можно классифицировать как:
- Тестирование компонентов в малом (CTIS — Component testing In Small): тестирование компонентов может проводиться с или без изоляции остальных компонентов в тестируемом программном обеспечении или приложении. Если это выполняется с изоляцией другого компонента, то это называется CTIS;
- Тестирование компонентов в целом (CTIL — Component testing In Large) — тестирование компонентов, выполненное без изоляции других компонентов в тестируемом программном обеспечении или приложении;
| Module/Unit testing | Component testing |
|---|---|
| Тестирование отдельных классов, функций для демонстрации того, что программа выполняется согласно спецификации | Тестирование каждого объекта или частей программного обеспечения отдельно с или без изоляции других объектов |
| Проверка в(на) соответствии с design documents | Проверка в(на) соответствии с test requirements, use case |
| Пишутся и выполняются разработчиками | Тестировщиками |
| Выполняется первым | Выполняется после Unit |
Другой источник:
По-существу эти уровни тестирования представляют одно и тоже, разница лишь в том, что в компонентном тестировании в качестве параметров функций используют реальные объекты и драйверы, а в модульном/unit тестировании — конкретные значения.
*В контексте юнит-тестирования еще можно встретить понятие golden testing. Оно означает те же юнит тесты, но с ожидаемыми результатами хранящимися в отдельном файле. Таким образом после прогона выходные значения тестов сравниваются с golden (эталонным) файлом.
*Иногда юнит-тесты называют одинокими (solitary) в случае тотального применения имитаций и заглушек или общительными (sociable) в случае реальных коммуникаций с другими участниками.
*Правило трех А(AAA) (arrange, act, assert) или триада «дано, когда, тогда» — хорошая мнемоника, чтобы поддерживать хорошую структуру тестов.
Источники:
- What is Component Testing? Techniques, Example Test Cases
- Лекция 5: Модульное и интеграционное тестирование
Доп. материал:
- ГОСТ Р 56920-2016/ISO/IEC/IEEE 29119-1:2013 “D.11 Подпроцесс покомпонентного тестирования”
- Я сомневался в юнит-тестах, но…
- Юнит-тесты переоценены
- Elliotte Rusty Harold — Effective Unit Testing
- Kevlin Henney — What we talk about when we talk about unit testing
- Андрей Сербин — Компонентное тестирование инфраструктуры
- Анатомия юнит тестирования
- Unit Test
- Component Test
- Анатомия юнит-теста
- Почему большинство юнит тестов — пустая трата времени? (перевод статьи)
- Unit Testing Guide
- Лекция 2: Тестирование программного кода (методы+окружение)
- Starting to Unit Test: Not as Hard as You Think
- 6 оправданий для того, чтобы не писать юнит-тесты
- Принципы юнит-тестирования. Часть первая