пятница, 13 мая 2016 г.

[prog.c++14] Шаблоны против копипасты-5: variadic templates упрощают написание unit-тестов

В процессе развития одной библиотеки появилось время написать unit-тесты. Библиотека представляет из себя сборище классов, экземпляры которых используются в качестве MQ-шных сообщений. В качестве формата данных -- JSON. Нужно проверить правильно ли происходи сериализация и десериализация каждого типа сообщения.

Если использовать тупой метод грубой силы, то получилось бы что-то вроде (названия типов и полей искажены):

TEST_CASE( "initiate_action pack/unpack""initiate_action" )
{
   const std::vector< int > variants{ 32 };
   msg_ns::initiate_action n1;

   n1.m_action_id = "1";
   n1.m_actor_host = "localhost";
   n1.m_action_strategy = msg_ns::SIMPLE_STRATEGY;
   n1.m_duration = 123;
   n1.m_retries = 5;
   n1.m_attempt_timeout = 20;
   n1.m_attempt_pause = 300;
   n1.m_variants = variants;

   string n1_str = to_json( n1 );

   auto n2 = from_json< msg_ns::initiate_action >( n1_str );

   REQUIRE( n1.m_action_id == n2.m_action_id );
   REQUIRE( n1.m_actor_host == n2.m_actor_host );
   REQUIRE( n1.m_action_strategy == n2.m_action_strategy );
   REQUIRE( n1.m_duration == n2.m_duration );
   REQUIRE( n1.m_retries == n2.m_retries );
   REQUIRE( n1.m_attempt_timeout == n2.m_attempt_timeout );
   REQUIRE( n1.m_attempt_pause == n2.m_attempt_pause );
   REQUIRE( n1.m_variants == n2.m_variants );

   string n2_str = to_json( n2 );

   REQUIRE( n1_str == n2_str );
}

TEST_CASE( "initiate_action_ack pack/unpack""initiate_action_ack" )
{
   msg_ns::initiate_action_ack n1;

   n1.m_actor_id = 1;

   string n1_str = to_json( n1 );

   auto n2 = from_json< msg_ns::initiate_action_ack >( n1_str );

   REQUIRE( n1.m_actor_id == n2.m_actor_id );

   string n2_str = to_json( n2 );

   REQUIRE( n1_str == n2_str );
}

Очевидно, что писать такие процедуры с кучей копипасты очень не хочется. Поэтому, проделав несколько итераций в итоге пришел вот к такому варианту на базе variadic templates:

TEST_CASE( "initiate_action pack/unpack""initiate_action" )
{
   using T = msg_ns::initiate_action;
   const std::vector< int > variants{ 32 };

   do_test< T >(
      &T::m_action_id, "1",
      &T::m_actor_host, "localhost",
      &T::m_action_strategy, msg_ns::SIMPLE_STRATEGY,
      &T::m_duration, 123,
      &T::m_retries, 5,
      &T::m_attempt_timeout, 20,
      &T::m_attempt_pause, 300,
      &T::m_variants, variants );
}

TEST_CASE( "initiate_action_ack pack/unpack""initiate_action_ack" )
{
   using T = msg_ns::initiate_action_ack;
   do_test< T >( &T::m_actor_id, 1 );
}

Вся магия скрывается в шаблонной функции do_test. Эта функция получает на вход последовательность пар значений (указатель на поле, значение поля). После чего конструирует объект заданного типа, инициализирует его поля используя переданную ей последовательность. Ну и выполняет логику теста.

Реализация этой шаблонной магии выглядит так:

templatetypename T >
void
fill_fields( T & ) {}

templatetypename T, typename F, typename V, typename... PAIRS >
void
fill_fields( T & o, F f, const V v, PAIRS &&... pairs )
{
   o.*f = v;
   fill_fields( o, std::forward<PAIRS>(pairs)... );
}

templatetypename T >
void
compare_fields( const T & ) {}

templatetypename T, typename F, typename V, typename... PAIRS >
void
compare_fields( const T & o, F f, const V v, PAIRS &&... pairs )
{
   REQUIRE( o.*f == v );
   compare_fields( o, std::forward<PAIRS>(pairs)... );
}

templatetypename T, typename... PAIRS >
void do_test( PAIRS &&... pairs )
{
   T n1;
   fill_fields( n1, std::forward<PAIRS>(pairs)... );

   string n1_str = to_json( n1 );

   auto n2 = from_json< T >( n1_str );

   compare_fields( n2, std::forward<PAIRS>(pairs)... );

   string n2_str = to_json( n2 );

   REQUIRE( n1_str == n2_str );
}

Аналогичного эффекта можно было бы достичь и с использованием макросов с переменным количеством параметров. Но макросы я не очень люблю. Да и в случае с шаблонами компилятор более вменяемую диагностику выдает в случае чего.

PS. В качестве библиотеки для unit-тестирования используется отличная штука под названием Catch.

PPS. У меня уже целая серия постов "Шаблоны против копипасты" образовалась. Предыдущая часть здесь.

Комментариев нет: