Техника автоматического тестирования "для бедных"

В этом разделе предлагается универсальный, но достаточно примитивный метод автоматического (регрессионного) юнит-тестирования с помощью простых сценариев операционной системы ("скриптов") и программы make.

В качестве примера тестируемой программы рассмотрим класс ZZ, реализующий арифметику многократной точности.

Тестирование корректности выполнения операций

Тесты для проверки корректности выполнения арифметических действий будут состоять из сложения и умножения положительных и отрицательных чисел:

Для этого мы напишем специальную программу test-correct, которая получает на вход через стандартный поток ввода stdin описание решаемой задачи, вызывает тестируемую функцию с нужными аргументами и выводит полученный ответ в stdout. Будем считать, что одна строчка входного потока переводится этой программой в одну строчку выходного потока. Например, если на вход поступает последовательность строк, приведенная слева, то на выходе получается поток, представленный справа:

входной поток

* 2 3
+ 2 5

выходной поток

6
7
Все операции производятся с объектами класса ZZ, независимо от значений поступающих чисел.

Поскольку тестов на проверку корректности основных операций может быть много (в нашем примере есть как минимум шесть групп тестов, различающихся знаками и разрядностью чисел), а выполняются они единообразно, то для обработки всех тестов можно использовать следующую процедуру. Создадим директорию с именем UNIT-TESTS, содержащую множество файлов с входными данными для программы test-correct. Пусть эти файлы имеют имена вида название группы тестов.in. Например, файл 32bits_positive.in будет содержать тесты на выполнение действий с "короткими" положительными целыми числам. В файл с тем же именем, но расширением .out запишем ожидаемые, то есть корректные ответы. Файл 32bits_positive.out содержит правильные ответы для файла 32bits_positive.in. В этом случае выполнение тестов сводится к вызову программы test-correct для in-файла и сравнения результата работы с содержимым соответствующего out-файла. Это делает приведенный ниже сценарий оболочки bash.

Содержимое файла run-tests
#!/bin/bash
test_runner=$1
tests_directory=${2:-./UNIT-TESTS}
tmp=last_run_results.txt
nfailed=0

for testfile in `find $tests_directory -type f -name "*\.in"`
do
  testname=`basename $testfile .in`
  expected_answer=`dirname $testfile`/`basename $testfile .in`.out
  echo -E -n $testname " "
  $test_runner <$testfile 2>/dev/null >$tmp
  diff -abBiq $tmp $expected_answer >/dev/null
  if [ $? -ne 0 ]
  then
    echo -e \\033[31m \\t "FAILED" \\033[0m
    ((nfailed++))
  else
    echo -e \\033[32m \\t "PASSED" \\033[0m
  fi
done 
exit nfailed
Файл нужно пометить как исполняемый, выполнив команду операционной системы chmod +x run-tests

Тестирование программы при некорректных входных данных

Если программа test-correct выводит предсказуемое сообщение об ошибке в случае поступления некорректных входных данных, то для тестов этого типа можно использовать подход с in- и out- файлами. Для этого достаточно создать файлы вида:

errors.in

* 2.1 3
+ 1 5abc

errors.out

not an integer
not an integer
Здесь предполагается, что программа test-correct печатает сообщение "not an integer", если конструктор класса ZZ не смог создать объект из заданной строки. Отметим, что проверку корректности входных данных должен делать класс ZZ, а не наша вспомогательная программа test-correct, так как в противном случае мы будем тестировать ее, а не сам класс.

Тестирование на случайной последовательности действий

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

ZZ x;

for(int k = 0; k < MAX_ITERATIONS; k++) {
    int operation = random() % 5;
    ZZ y(random());
    switch(operation) {
        case 0:
        case 1:
            x = y + x;
            break;
        case 2: {
            ZZ temp = x*x;
            temp = x*x;
            x = (x*y) + temp;
            break;
        }
        // ...
    }
}
Переменная operation на каждой итерации принимает значение из множества {0, 1, 2}. Если это 0 или 1, то выполняется одно действие. Если значение равно 2, то другое. И так далее. Заметим, что operation имеет равномерное распределение, а за счет указания нескольких меток case можно повысить вероятность выполнения некоторых операций. На каждой итерации создается один (y) или два (y, temp) объекта класса ZZ. Переменная x используется для накопления значений.

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

Для автоматизации процесса тестирования воспользуемся возможностями команды make. Для этого добавим в Makefile цель tests, которая будет выполнять все тесты.

.PHONY: tests # Эту строчку принято писать в начале Makefile
tests: test-stability test-errors
        ./run-tests test-correct # Тесты операций и некорректных данных
        ./test-stability
        echo "Все тесты пройдены!"

Здесь предполагается, что у нас есть три специально написанные программы. Программы test-stability и test-errors выполняют длнную последовательность операций и операции с некорректными входными данными, соответственно. В случае успешного выполнения тестов эти программы возвращают 0, а в случае ошибки – ненулевое значение. Это гарантирует, что команда echo будет выполнена только в случае корректного завершения всех тестов.

Накопление тестов

Регрессионное тестирование подразумевает накопление тестов. В предлагаемой схеме тестирования накопление может осуществляться двумя способами.

  1. Добавление новых тестов с корректными и некорректными входными данными (при условии, что тестирующая программа выдает предсказуемое сообщение при поступлении некорректных данных) производится простым добавлением in- и out-файлов.
  2. Написание новых специальных программ тестирования и добавление их в Makefile.
Оба способа достаточно просты в реализации, поэтому описанное решение может использоваться для регрессионного тестирования.