31 дек. 2011 г.

CUnit - фреймворк для юнит-тестирования программ на C

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

Когда я писал на Clojure, мне очень нравилась встроенная в него в система для проведения юнит-тестирования. До этого я не пользовался юнит-тестами и идея писать некоторые функции, которые проверяют работоспособность моего кода на некотором наборе входных значений, а также, по совместительству, являются готовыми примерами использования, мне весьма приглянулась.
Юнит-тестированием для кода, написанного на Си, я еще ни разу не занимался и не знал что там принято использовать. Задав вопрос в своем G+ я получил совет попробовать CUnit - http://cunit.sourceforge.net. О моем небольшом опыте использования этого фреймворка для юнит-тестирования и пойдет речь в моем сегодняшнем посте...

Для начала нужно установить CUnit. Пакета для Slackware я не нашел - пришлось собирать все из сорцов (скачать исходники можно отсюда: http://sourceforge.net/projects/cunit/). Обычная сборка выполняется как обычно: ./configure && make && make install.
Чтобы собрать готовый пакет для Slackware следует выполнить несколько иную последовательность действий:
  1. ./configure --prefix /tmp/cunit
  2. make
  3. make install
  4. cd /tmp/cunit
  5. makepkg ../cunit-2.1.2.tgz
    На все вопросы отвечаем 'yes'.
  6. sudo installpkg ../cunit-2.1.2.tgz
Теперь немного теории. Все юнит-тесты в CUnit'е объединяются в наборы (suite), которые в свою очередь все объединяются в один большой реестр (registry). Таким образом, можно объединять вместе юнит-тесты, которые совпадают например по проверяемой функциональности - тесты, проверяющие функции чтения и записи в канал, будут в одном наборе, а проверяющие функции работы с псевдотерминалом - в другом наборе. И все эти наборы будут объединены в один большой реестр.
Выглядит все это примерно так (схема взята из официальной документации):

                      Test Registry
                            |
             ------------------------------
             |                            |
          Suite '1'      . . . .       Suite 'N'
             |                            |
       ---------------             ---------------
       |             |             |             |
    Test '11' ... Test '1M'     Test 'N1' ... Test 'NM'

Каждый юнит-тест представляет собой процедуру вида void unittest_func1(void) внутри которой происходит проверка некоторой, одной функции программы. Проверка может осуществляться при помощи разнообразных, "контролирующих" операторов, которые описаны здесь: http://cunit.sourceforge.net/doc/writing_tests.html#assertions.

Рассмотрим все вышесказанное на небольшом примере. Допустим, у нас есть функции readn() и writen(), которые гарантированно (по возможности) читают N байт из дескриптора. Напишем пару юнит-тестов, в которых проверяется, действительно ли эти функции записали/прочитали столько байт, сколько было нужно.
В первом тесте мы будем тестировать функцию writen(), которая будет писать некоторые слова в канал, а во втором тесте, соответственно, мы будем тестировать функцию readn(), которая будет читать из канала и проверять - совпадают ли прочитанные слова с тем, что мы записывали в первом тесте.

Метод тестирования функции writen() прост - проверяем, открыт ли канал и пишем в него различные символы, проверяя при этом, действительно ли мы записали столько символов, сколько хотели. При проведении тестирования CU_ASSERT вернет ошибку, если условие, записанное внутри него, будет ложным.

Тест для функции readn() выглядит похоже, за исключением того, что мы проверяем прочитанное из канала при помощи CU_ASSERT_NSTRING_EQUAL.

Как видно из вышеприведенных примеров, мы читаем и пишем в канал для проверки наших функций. Но откуда этот канал взялся, ведь внутри юнит-тестов нет вызовов pipe() или подобных функций?
Фреймворк cunit позволяет для каждого из наборов задавать функции выполняющиеся перед запуском тестов из набора и после окончания всех этих тестов. В этих функциях можно открывать файлы с тестовым набором данных, создавать каналы и так далее. И именно в этих функциях в нашем примере создается канал и уничтожается по окончании тестирования.

Теперь, объединим все эти разрозненные функции вместе. Нам нужно создать реестр тестов, включить в него наш набор тестов для функций readn/writen, задать для этого набора вышеприведенные функции инициализации и завершения работы, добавить в набор нашу пару юнит-тестов и наконец - запустить все это на выполнение. Стоит отметить, что юнит-тесты выполняются в порядке добавления в набор - то есть сначала выполнится test_writen(), а затем test_readn(), как нам и необходимо.
CUnit может выводить результаты в XML-файл, на консоль или же использовать curses или графический интерфейс. Я буду использовать самый базовый способ - вывод на консоль.

Теперь, для удовлетворительной работы теста, осталось подключить к нему заголовочный файл CUnit/Basic.h и скомпилировать его, не забыв прилинковать библиотеку libcunit.so. Я делаю это Makefile'ом следующего содержания:

После запуска файла io_test мы увидим ход выполнения тестов на консоли:

Если во время тестирования будут обнаружены ошибки, то это будет отмечено в столбце "Failed". Как видно, в данном тесте у нас не обнаружена ни одна ошибка - значит, можно спокойно привносить новые.

За дополнительной информацией по этому фреймворку для юнит-тестирования крайне рекомендую обратиться к официальной документации, она крайне простая, понятная и весьма короткая: http://cunit.sourceforge.net/doc/index.html.
Также, рекомендую посмотреть на пример использования CUnit: http://cunit.sourceforge.net/example.html