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

Когда я писал на 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

C linux