В процессе развития одной библиотеки появилось время написать 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. Эта функция получает на вход последовательность пар значений (указатель на поле, значение поля). После чего конструирует объект заданного типа, инициализирует его поля используя переданную ей последовательность. Ну и выполняет логику теста.
Реализация этой шаблонной магии выглядит так:
template< typename T > void fill_fields( T & ) {} template< typename 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)... ); } template< typename T > void compare_fields( const T & ) {} template< typename 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)... ); } template< typename 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. У меня уже целая серия постов "Шаблоны против копипасты" образовалась. Предыдущая часть здесь.
Комментариев нет:
Отправить комментарий