Когда я писал на Clojure, мне очень нравилась встроенная в него в система для проведения юнит-тестирования. До этого я не пользовался юнит-тестами и идея писать некоторые функции, которые проверяют работоспособность моего кода на некотором наборе входных значений, а также, по совместительству, являются готовыми примерами использования, мне весьма приглянулась.
Юнит-тестированием для кода, написанного на Си, я еще ни разу не занимался и не знал что там принято использовать. Задав вопрос в своем G+ я получил совет попробовать CUnit - http://cunit.sourceforge.net. О моем небольшом опыте использования этого фреймворка для юнит-тестирования и пойдет речь в моем сегодняшнем посте...
Для начала нужно установить CUnit. Пакета для Slackware я не нашел - пришлось собирать все из сорцов (скачать исходники можно отсюда: http://sourceforge.net/projects/cunit/). Обычная сборка выполняется как обычно: ./configure && make && make install.
Чтобы собрать готовый пакет для Slackware следует выполнить несколько иную последовательность действий:
- ./configure --prefix /tmp/cunit
- make
- make install
- cd /tmp/cunit
- makepkg ../cunit-2.1.2.tgz
На все вопросы отвечаем 'yes'. - sudo installpkg ../cunit-2.1.2.tgz
Теперь немного теории. Все юнит-тесты в CUnit'е объединяются в наборы (suite), которые в свою очередь все объединяются в один большой реестр (registry). Таким образом, можно объединять вместе юнит-тесты, которые совпадают например по проверяемой функциональности - тесты, проверяющие функции чтения и записи в канал, будут в одном наборе, а проверяющие функции работы с псевдотерминалом - в другом наборе. И все эти наборы будут объединены в один большой реестр.
Выглядит все это примерно так (схема взята из официальной документации):
Тест для функции 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
Выглядит все это примерно так (схема взята из официальной документации):
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