Простое unit-тестирование на C++ (thanks to mitya57)

Mitya57 написал небольшую библиотеку для проведения автоматического тестирования корректности простых выражений на C++. Для использования этой библиотеки в своей программе Вам достаточно скачать два заголовочных файла: testing.hpp  и printing.hpp  . В первом файле содержатся собственно функции для тестирования, во втором — вспомогательные данные для красивой (цветной) печати результатов тестирования.

Реализацию самой системы тестирования мы рассматривать не будем. Желающие могут самостоятельно посмотреть на содержимое указанных выше файлов. Далее описывается то, как нужно пользоваться этой библиотекой.

Подготовка тестов

Тестовая программа состоит из одного или более теста (TestCase). Каждый тест включает проверку одного или более элементарных условий. Для задания теста необходимо:

Ниже приведен минимальный пример, который определяет один тест, проверяющий корректность выполнения стандартных арифметических операций. Проверка равенства значений двух выражений выполняется вызовом ASSERT_EQUAL.

#include "testing.hpp"

struct test_FirstAndTrivial: TestCase {
  void run() override {
    unsigned int val = 1;
 
    ASSERT_EQUAL((val + 1) / 2, 1u);
  }
};

REGISTER_TEST(test_FirstAndTrivial, "Arithmetic operations");

Это законченный пример, то есть его можно откомпилировать и запустить, о чем написано чуть ниже. Обратите внимание на его простоту. Для создания теста достаточно написать несколько строк кода и включить один заголовочный файл.

В данном примере создается тест test_FirstAndTrivial (имя класса может быть любым). Напомним, что структуры являются классами, у которых все поля общедоступны. Функция run, которая вызывается автоматически для каждого зарегистрированного теста, выполняет все предусмотренные для данного теста проверки. Функция run может производить любые вычисления и вызывать доступные макросы для проверки корректности выполнения операций. Регистрация теста REGISTER_TEST приписывает этому классу краткое текстовое описание, которое будет выводиться на экран в процессе выполнения теста.

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

Запуск тестов и результат их выполнения

Заголовочный файл testing.hpp содержит реализацию функции main, поэтому приведенный выше код является законченным примером. Предположим, что он записан в файле с именем trivial_test.cpp и в этой директории находятся оба hpp-файла системы тестировани. Для компиляции программы-тестера можно использовать команду

g++ --std=c++11 -o trivial_test trivial_test.cpp
Поскольку в тексте тестера используются современные конструкции языка C++, то при компиляции нужно указать флаг --std=c++11, который определяет используемую версию языка C++ (стандарт 2011 года).

После запуска ./trivial_test на экран будет выведено:

* Arithmetic operations
  Result: SUCCESS (1 assertions passed)
Это сообщение означает, что все проверки, включенные в тест, который был зарегистрирован с именем "Arithmetic operations", были вполнены успешно.

Теперь добавим в наш тест новую проверку ASSERT_EQUAL(val+2, 1+1), которая, очевидно, не выполняется при значении переменной val = 1. После запуска измененной программы мы получим сообщение вида:

* Arithmetic operations
  ERROR in `test_FirstAndTrivial::run` at /path_to_file/testing_example.cpp line 13:
    Expressions `val+2' and `1+1' are not equal.
    val+2 = 3, 1+1 = 2
  Result: FAIL (1 of 2 assertions passed)
Здесь видно, что ошибка произошла в строке 13 файла testing_example.cpp и функции test_FirstAndTrivial::run. Далее печатаются выражения, знаения которых не совпали. Выводится как исходная форма выражений, наприимер, val+2, так и полученные значения.

Описание доступных примитивов

Более сложные тесты

Реализация теста внутри функции run удобно для коротких тестов. Если для тестирования какого-либо свойства системы или класса требуется выполнять значительный объем вычислений, то удобнее разделять тесты на части. Во-первых, в одной программе можно создать и заегистрировать несколько классов, унаследованных от TestCase. Во-вторых, каждый класс может содержать произвольное число вспомогательных методов.

Приведенный ниже "каркас" тестирующей программы иллюстрирует эту идею.

#include "testing.hpp"
// дополнительные директивы #include
// для Ваших файлов

struct test_Something: TestCase {
  // 
  int global_; // Общая для всех тестов переменная

  test_Something() {
    global_ = 123;
  }
  virtual ~test_Something() {
    // Что-то делаем после всех тестов
  }

  void do_to();
  void to_se();
  void do_pytoe();

  void run() override {
    do_to();
    global_ = 0;
    to_se();
    do_pytoe();
  }
};

void test_Something::to_to() {
  // Реализация
}
// ...


struct test_SomethingElse: TestCase {
    // ...
};

REGISTER_TEST(test_Something, "Tests for something");
REGISTER_TEST(test_SomethingElse, "Tests for something else");

Интеграция с Makefile

Тестирование является важной частью процесса разработки программ. Наличие автоматических тестов, которые выполняются без участия человека, позволяет проверять корректность программы практически после каждого изменения ее исходного кода. В крупных систмах выполнение тестов может занимать значительное время и тестирование выполняется с определенной периодичностью. Если считать, что тесты выполняются быстро, что верно для подавляющего большиства студенческих задач, то запуск тестов можно сделать автоматическим средствами Makefile.

Предположим, что Вы разрабатываете программу на основе класса X, и Ваш код записан в файлах X.h (объявление класса), X.cpp (реализация методов) и main.cpp (основная программа). Основу Вашей программы составляет класс X, поэтому корректность его работы необходимо гарантировать. Пусть дополнительно имеется два файла с описанием тестов, test_operation_1.cpp и test_operation_2.cpp, которые содержат тесты для двух операций класса X (в том смысле, как это описано на данной странице). В этом случае Makefile может иметь такой вид.

.PHONY: all test
# Стандартное правило компиляции (созание o-файла из cpp-файла с тем же именем)
# $< заменяется на имя cpp-файла, $@ - на имя o-файла)
%.o: %.cpp
    g++ -c -g $< -o $@

all: my_program test

test: test_operation_1 test_operation_2
    ./test_operation_1
    ./test_operation_2

my_program: X.o main.o
    g++ -o my_program X.o main.o

test_operation_1: test_operation_1.o X.o
    g++ --std=c++11 -o test_operation_1 test_operation_1.o X.o

test_operation_2: test_operation_2.o X.o
    g++ --std=c++11 -o test_operation_2 test_operation_2.o X.o

# Зависимости исходного кода от заголовочных файлов
test_operation_1.cpp: X.h
test_operation_2.cpp: X.h
X.cpp: X.h
main.cpp: X.h
Если выпоняется команда операционной системы make all (или просто make), то сначала производится компиляция программы my_program, потом, в случае успешного выполнения первого шага, производится компиляция тестовых программ test_operation_1 test_operation_2 (поскольку они указаны в части зависимостей для цели test). Если компиляция закончилась успешно, то тесты запускаются в указанном порядке и выполняются до первой ошибки. Если вызов ./test_operation_1 содержит тест, который не выполняется, то программа ./test_operation_2 не будет запускаться. Это возможно потому, что функция main, реализованная в файле testing.hpp, возвращает значение 0 только в случае успешного выполнения всех тестов.

Выполнение тестов до первой ошибки

По умолчанию система выполняет все тесты, а в заключении печатает отчет о числе выполненных и не выполненных проверок. Для запуска тестов в режие выполнения до первой найденной ошибки необходимо в командной строке указать ключ --fail-fast. Например,

./test_operation_1 --fail-fast