7.13 – Покрытие кода

Добавлено 30 мая 2021 в 14:30

В предыдущем уроке «7.12 – Введение в тестирование кода» мы обсудили, как писать и сохранять простые тесты. В этом уроке мы поговорим о том, какие тесты полезно писать, чтобы убедиться, что ваш код корректен.

Покрытие кода

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

Покрытие инструкций

Термин «покрытие инструкций» относится к проценту инструкций в вашем коде, которые были выполнены процедурами тестирования.

Рассмотрим следующую функцию:

int foo(int x, int y)
{
    int z{ y };
    if (x > y)
    {
        z = x;
    }
    return z;
}

Вызов этой функции как foo(1, 0) предоставит вам полное покрытие инструкций этой функции, поскольку будет выполняться каждая инструкция в функции.

Для нашей функции isLowerVowel():

bool isLowerVowel(char c)
{
    switch (c)        // инструкция 1
    {
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
        return true;  // инструкция 2
    default:
        return false; // инструкция 3
    }
}

Эта функция для проверки всех инструкций потребует двух вызовов, так как нет возможности достичь инструкций 2 и 3 в одном вызове функции.

Хотя стремление к 100% охвату инструкций – это хорошо, этого недостаточно для обеспечения правильности.

Покрытие ветвей

Покрытие ветвей относится к проценту выполненных ветвей, где каждая возможная ветвь учитывается отдельно. У оператора if есть две ветви: ветвь, которая выполняется, когда условие истинно, и ветвь, которая выполняется, когда условие ложно (даже если для выполнения нет соответствующей инструкции else). У оператора switch может быть много ветвей.

int foo(int x, int y)
{
    int z{ y };
    if (x > y)
    {
        z = x;
    }
    return z;
}

Предыдущий вызов foo(1, 0) обеспечил нам 100% покрытие инструкций и реализовал вариант, когда x > y, но это дает нам только 50% покрытие ветвей. Нам нужен еще один вызов foo(0, 1), чтобы проверить вариант, в котором инструкция if не выполняется.

bool isLowerVowel(char c)
{
    switch (c)
    {
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
        return true;
    default:
        return false;
    }
}

В функции isLowerVowel() потребуются два вызова, чтобы обеспечить 100% покрытие ветвей: один (например, isLowerVowel('a')) для проверки верхнего случая, а другой (например, isLowerVowel('q')) для проверки случая по умолчанию. Несколько случаев, которые относятся к одному и тому же телу, не нужно тестировать отдельно – если работает один, то и остальные должны работать.

Теперь рассмотрим следующую функцию:

void compare(int x, int y)
{
	if (x > y)
		std::cout << x << " is greater than " << y << '\n'; // случай 1
	else if (x < y)
		std::cout << x << " is less than " << y << '\n';    // случай 2
	else
		std::cout << x << " is equal to " << y << '\n';     // случай 3
}

Здесь необходимо 3 вызова, чтобы получить 100% покрытие ветвей:

  1. compare(1,0) проверяет положительный вариант использования для первого оператора if;
  2. compare(0, 1) проверяет отрицательный вариант использования для первого оператора if и положительный вариант использования для второго оператора if;
  3. compare(0, 0) проверяет отрицательный вариант использования для первого и второго операторов if и выполняет оператор else.

Таким образом, можно сказать, что эта функция может быть надежно протестирована 3 вызовами (что немного лучше, чем 18 квинтиллионов).

Лучшая практика


Стремитесь к 100% охвату ветвей вашего кода.

Покрытие цикла

Покрытие цикла (неофициально называемое тестом 0, 1, 2) говорит, что если у вас есть цикл в коде, вы должны убедиться, что он работает правильно, когда он повторяется 0 раз, 1 раз и 2 раза. Если он работает правильно для случая с двумя итерациями, он должен работать правильно для всех итераций, превышающих 2. Таким образом, эти три теста охватывают все возможные варианты (поскольку цикл не может выполняться отрицательное число раз).

Рассмотрим следующий код:

#include <iostream>
 
void spam(int timesToPrint)
{
    for (int count{ 0 }; count < timesToPrint; ++count)
         std::cout << "Spam! ";
}

Чтобы правильно протестировать цикл в этой функции, вы должны вызвать его три раза: spam(0) для проверки случая с нулем итераций, spam(1) для проверки случая с одной итерацией и spam(2) для проверки случая с двумя итерациями. Если spam(2) работает, то spam(n), где n > 2, тоже должен работать.

Лучшая практика


Используйте тест 0, 1, 2, чтобы убедиться, что ваши циклы работают правильно с разным количеством итераций.

Тестирование различных категорий входных данных

При написании функций, которые принимают параметры, или при принятии пользовательского ввода, подумайте, что происходит с разными категориями входных данных. В этом контексте мы используем термин «категория» для обозначения набора входных данных, которые имеют схожие характеристики.

Например, если бы я написал функцию для получения квадратного корня из целого числа, с какими значениями имеет смысл ее тестировать? Вероятно, вы начнете с какого-нибудь обычного значения, например 4. Но также неплохо было бы проверить ее с нулем и отрицательным числом.

Вот несколько основных рекомендаций по тестированию категорий:

Что касается целых чисел, убедитесь, что вы учли, как ваша функция обрабатывает отрицательные, нулевые и положительные значения. Вы также должны проверить наличие переполнения, если это актуально.

Для чисел с плавающей запятой убедитесь, что вы учли, как ваша функция обрабатывает значения, у которых есть проблемы с точностью (значения, которые немного больше или меньше ожидаемых). Хорошие тестовые значения – это 0.1 и -0.1 (для проверки чисел, которые немного больше ожидаемых) и 0.6 и -0.6 (для проверки чисел, которые немного меньше ожидаемых).

Для строк убедитесь, что вы учли, как ваша функция обрабатывает пустую строку (только нулевой терминатор), обычные допустимые строки, строки с пробелами и строки полностью из пробелов.

Если ваша функция принимает указатель, не забудьте также проверить nullptr (не беспокойтесь, если это вам непонятно, мы еще не рассмотрели его).

Лучшая практика


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

Небольшой тест

Вопрос 1

Что такое покрытие ветвей?

Покрытие ветвей – это процент выполненных ветвей, при этом положительный и отрицательный случаи учитываются отдельно.


Вопрос 2

Сколько тестов потребуется для минимальной проверки работы следующей функции?

bool isLowerVowel(char c, bool yIsVowel)
{
    switch (c)
    {
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
        return true;
    case 'y':
        return yIsVowel;
    default:
        return false;
    }
}

4 теста. Один для проверки случая a/e/i/o/u. Один для проверки случая по умолчанию. Один для проверки isLowerVowel('y', true). И один для проверки isLowerVowel('y', false).

Теги

C++ / CppLearnCppДля начинающихМодульное тестирование / Юнит-тестирование / Unit testingОбучениеПрограммированиеТестирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.