пятница, 28 мая 2010 г.

Boost data-driven test

Возникла необходимость проверить работу алгоритма на нескольких исходных данных. Поступил сигнал, что сервер не читает базы данных предыдущих версий. Сервер у нас пока сильно разобран, так что он может. Надо бы это дело потестировать юниттестами. Собрали базы данных разных версий, файлы Berkley DB по сути своей они.

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

Итак поехали...

Сперва это выливается в энное количество тестов следующего вида:
BOOST_AUTO_TEST_CASE(import_3_database)
{
  BOOST_REQUIRE_EQUAL(DbProxy("bakup-3.db").getServerVersion(), 3);
  BOOST_REQUIRE_NO_THROW(DbProxy("bakup-3.db").dbConvert());
}
Очень однообразных, и немного более навороченных, тестов...

Здесь надо применить Custom assertion! В результате чего мы получаем меньше кода, но однообразия меньше не становится:
BOOST_AUTO_TEST_CASE(import_3_database)
{
  CSTOM_REQUIRE_CONVERT("bakup-3.db", 3);
}
Но зато становится ясно, что все, что различает наши тесты между собой - это имя файла и номер версии. Значит необходимо сделать из всего этого управляемый данными тест. Но поскольку это достаточно редкая техника, в boost она слабо развита.

Можно было бы написать так:
BOOST_AUTO_TEST_CASE(import_database)
{
  CUSTOM_REQUIRE_CONVERT("bakup-3.db", 3);
  CUSTOM_REQUIRE_CONVERT("bakup-4.db", 4);
  ...
}
Но это было бы ошибкой, которая заключается в том, что ошибка в первом тесте автоматически приведет к невыполнению всех последующий. Можно было бы использовать CHECK вместо REQUIRE, но я предпочитаю REQUIRE и не ищу легких путей. Предпочитаю правильные пути...

Есть в boost штука, которая называется BOOST_PARAM_TEST_CASE. Это генератор (как у меня блин питоновая терминология прет), который по функции и паре итераторов генерирует тестовый набор. Но есть несколько проблем.

Во первых тестовая функция принимает только один параметр - значение из текущего итератора, следовательно прототип CUSTOM_REQUIRE_CONVERT нас уже не устраивает, потому что требует двух параметров. Можно упаковать два наших параметра в объект, но я для простоты взял std::pair.
typedef pair<const char *, uint32_t> dbe;
Сформировав набор тестовых данных
static dbe dba[] = {
 dbe("bakup-3.db", 3),
 dbe("bakup-4.db", 4),
 ...
};
Мы можем получить из них тестовый набор
BOOST_PARAM_TEST_CASE(testConversion, 
 dba, dba + sizeof(dba) / sizeof(dbe))
Где testConversion, это собственно новая тестовая функция, которая пришла на замену CUSTOM_REQUIRE_CONVERT, и принимает const dbe & вместо пары значений.

Но наличие тестового набора не дает нам ровным счетом ничего, пока мы не добавили его в главный тестовый набор. Честно скажу, подходящего макроса в BOOST TEST я не нашел. И чтобы не изобретать свой велосипед - начал рыться в чужом. Где выяснилось, что BOOST_AUTO_TEST_CASE для своей саморегистрации использует BOOST_AUTO_TU_REGISTRAR, который нам в чистом виде тоже не подходит, Но если порыть еще глубже, то мы можем докопаться до boost::unit_test::ut_detail::auto_test_unit_registrar, который в одном из своих конструкторов принимает в частности test_unit_generator const &, который возвращается BOOST_PARAM_TEST_CASE.

Попробуем все это объединить:
static ut_detail::auto_test_unit_registrar registrar
 (BOOST_PARAM_TEST_CASE(testConversion, 
  dba, dba + sizeof(dba) / sizeof(dbe)));

И наступило почти счастье, каждый тесткейс запускается изолированно, Правда все они носят имя testConversion. Наверное индивидуальный их запуск будет затруднителен. Да, так и есть.
./Runner --log_level=test_suite --run_test=*/testConversion                                                                                                   
Running 6 test cases...
...
Ну за все приходится платить. В данном случае мы получаем мало кода, что уже хорошо.