вторник, 5 ноября 2013 г.

Юнит тестинг по моему

В основном я пользуюсь boost::test. Это в принципе неплохой фреймворк, но не так давно, занимаясь хобби проектиком подумал... Последнее время для себя все больше пишу на новом C++, и часто получается так, что boost тащится только ради тестирования. И более того, если хочется чего-то необычного - докумментация boost::test не дает ответов на эти вопросы. Нужно лезть в исходники и разбираться, например как вызвать все тесты из своего main? Как в один тест запихнуть несколько разных параметров?
Закончилось все тем, что решил написать свой фреймворк, с блекджеком и ш.. параметризованными тестами и сравнением коллекций...

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

Синглтон от GoF требует .сpp файл для описания статического указателя - не вариант. А вот синглтон Майерса - прекрасно живет в инклюде.

class TestCollection {
 static TestCollection &getInstance() {
  static TestCollection collection;
  return collection;
 };

Вторым встал вопрос о том, как реализовать параметризованные тесты. boost умеет параметризовать тесты, но выглядит это не очень изящно. Мне же хотелось увидеть минимум букв, что-то типа того:

const auto params = {
 { 1, "one" },
 { 2, "two" }
};

UP_PARAMETRIZED_TEST(test, params)
{
 ...
}

Но реальность жестока... компилятор не может типизировать braces list во что-то конкретное, кроме std::initializer_list, но std::initializer_list должен состоять из элементов одного типа. Я должен явно сказать, что список состоит из std::tuple. И исходный список параметров трансформировался в:

const auto params = {
 make_tuple(1, "one"),
 make_tuple(2, "two")
};

Но и дальше не все так просто... для разворачивания макро UP_PARAMETRIZED_TEST мне необходимо явно прописать из чего состоит этот тупл.

UP_PARAMETRIZED_TEST(test, params, int, const char *)
{
 ...
}

Хотя возможно я погорячился и смогу убрать это чуть позже, может быть шаблоны прокатят. Но были какие-то причины и сложности. Tuple сохраняет строку как указатель на char, она не хочет сама по себе превращаться в string... пичаль. Внутри теста параметры раcтупливаются обычным способом. И их может быть произвольное количество.

static const auto encrypt_params = {
 make_tuple(key01, 0xCCCCCCCCU, 0x33333333U, 0xF5FE5211U, 0x17E8D02EU),
 make_tuple(key01, 0x33333333U, 0xCCCCCCCCU, 0x6390ED97U, 0x3A962C89U),
 make_tuple(key02, 0xCCCCCCCCU, 0x33333333U, 0x2A78B7E0U, 0x800A0268U),
 make_tuple(key02, 0x33333333U, 0xCCCCCCCCU, 0x462DA336U, 0xEAB90129U),
 make_tuple(key03, 0xCCCCCCCCU, 0x33333333U, 0x8BB8CF97U, 0x533CDA6BU),
 make_tuple(key03, 0x33333333U, 0xCCCCCCCCU, 0xBE407AB5U, 0x5C055B4FU),
 make_tuple(key04, 0xCCCCCCCCU, 0x33333333U, 0x895A9742U, 0x02DB134CU),
 make_tuple(key04, 0x33333333U, 0xCCCCCCCCU, 0xDAA70325U, 0xB95DDC39U),
 make_tuple(key05, 0xCCCCCCCCU, 0x33333333U, 0x401EBED9U, 0x56F5D77DU),
 make_tuple(key05, 0x33333333U, 0xCCCCCCCCU, 0x4E790503U, 0x73FE0118U),
};

UP_PARAMETRIZED_TEST(encryptShouldBeCorrect, encrypt_params,
       vector<uint8_t>, uint32_t, uint32_t, uint32_t, uint32_t)
{
 const auto key = get<0>(encrypt_params);
 const auto A = get<1>(encrypt_params);
 const auto B = get<2>(encrypt_params);
 const auto eA = get<3>(encrypt_params);
 const auto eB = get<4>(encrypt_params);

По моему не плохо получилось...

Фикстуры реализуются достаточно просто, поэтому я и параметризованные тесты дополнил фикстурами, если необходимо.

Пришлось изрядно повозиться с универсальным сравнителем. Очень раздражает BOOST_REQUIRE_COLLECTIONS_EQUAL. У меня все сравнивается через UP_ASSERT_EQUAL (ну или не сравнивается через UP_ASSERT_NE). В него можно передать числа, строки, коллекции разных типов. Коллекции сравниваются по содержимому. Если что-то не сравнивается - то ошибки компиляции не будет - при запуске сфейлится утверждение и напечатает всю ту шнягу, которую в него запихнули.

Интересная история с ASSERT_EXCEPTION. Этот метод я посчитал необходимым, поскольку часто им пользуюсь. Но передавать в макро код, который должен выполниться в рамках try/catch - cчитаю крайне неестественным. Если мы хотим передать куда-то код - мы должны описать блок кода, как будто наше макро - часть языка. Или на худой конец передать сallable object, чтобы было похоже что наше макро - функция.

Первый вариант на первый взгляд показался совершенно нереализуемым:

UP_ASSERT_EXCEPTION(std::exception) {
 ...
};

Но мне кажется, что такое макро можно написать. Просто сразу что-то не заработало и я решил не заморачиваться. К тому же мучают сомнения насчет того, какой вариант более естественно выглядит для плюсов. Остановился на варианте с лямбдами.

UP_ASSERT_EXCEPTION(std::exception, []{
 ...
});
UP_ASSERT_EXCEPTION(std::exception, message, []{
 ...
});

Тесты можно группировать в наборы, но выборочное выполнение пока не сделано.

И чтобы вообще не заморачиваться - в одном из тестовых модулей нужно написать:

UP_MAIN();

этот стандартный main поддерживает тихий запуск теста, тайминг по тестам, определение порядка выполнения тестов (через рандом сид) и все. Ничего лишнего.

Не стал делать сравнение с плавающей точкой. Во первых сам почти не работаю с плавающей точкой. Во вторых моя универсальная функция сравнения принимает только два параметра, описывать отдельную - помоему не очень хорошая идея. Любое, самое извращенное сравнение можно реализовать через UP_ASSERT. :)

На этом пока все. Follow me on GitHub.