Табличные юнит тесты
Aug 1, 2018 · CommentsПрограммированиеТестированиеЮнит тесты
Расскажу об одной технике, которая, как мне кажется, позволяет писать более понятные и читаемые юнит тесты. Начну немного издалека. Допустим мы хотим убедится, что определенный кусок кода работает правильно. Если бы мы имели дело с игрушечным примером, то это наш тест мог бы Примерно вот так:
bool foobar(int x, int y)
{
return (x + y) > 0 && ((x + y) % 3) == 0;
}
ASSERT_FALSE(foobar(0, 0));
ASSERT_FALSE(foobar(0, 1));
ASSERT_FALSE(foobar(0, 2));
ASSERT_TRUE (foobar(0, 3));
ASSERT_FALSE(foobar(1, 0));
ASSERT_FALSE(foobar(1, 1));
ASSERT_TRUE (foobar(1, 2));
ASSERT_FALSE(foobar(2, 0));
ASSERT_TRUE (foobar(2, 1));
ASSERT_FALSE(foobar(2, 2));
...
Такой тест достаточно легко читать. Достаточно понять конструкцию
ASSERT_XXX(foobar(...))
после чего глаза сами фокусируются на входных
значениях, автоматически отсеивая ненужный синтаксический мусор.
Однако, если взять реальный код, то подобный пример превратиться в гораздо менее удобочитаемого монстра:
std::unique_ptr<Foobar> rabbit =
create_foobar_with_abc(parent,
some_context,
more_stuff + that + is_required + to_create_foobar,
a,
b,
mock_c(),
logger);
ASSERT_TRUE(
rabbit->connect(
this_thing,
that_key,
did_you_think_we_are_done_here ? no : hell_no));
ASSERT_TRUE(rabbit->wait_for_accept(timeout));
ASSERT_FALSE(rabbit->foobar(0, 0));
… и так далее для каждого тест кейса. Подобный код легко превращается в длинную простыню однообразного текста с незаметными, но важными вариациями во входных данных или параметрах инициализации.
Чтобы сделать этот код более читаемым нужно его разделить на разные по назначению части: собрать однообразный код инициализации в одном месте, а набор входных значений для теста - в другом. Например вот так:
struct {
int x;
int y;
std::string that_thing;
std::string this_key;
bool result;
} const cases[] = {
{0, 0, "thing1", "key2", false},
{0, 1, "thing1", "key2", false},
{0, 2, "thing1", "key2", false},
{0, 3, "thing1", "key1", true },
{1, 0, "thing2", "key1", false},
{1, 1, "thing2", "", false},
{1, 2, "thing2", "", true },
{2, 0, "", "", false},
{2, 1, "", "", true },
{2, 2, "", "", false},
};
for (const auto& c : cases)
{
std::unique_ptr<Foobar> rabbit =
create_foobar_with_abc(parent,
some_context,
more_stuff + that + is_required + to_create_foobar,
a,
b,
mock_c(),
logger);
if (!c.that_thing.empty())
{
ASSERT_TRUE(
rabbit->connect(
c.this_thing,
c.that_key,
did_you_think_we_are_done_here ? no : hell_no));
ASSERT_TRUE(rabbit->wait_for_accept(timeout));
}
ASSERT_EQ(rabbit->foobar(c.x, c.y), c.result);
}
Смотрите что получилось. Код инициализации стал сложнее, так так теперь он поддерживает все варианты инициализации, используемые в тесте. Но его нужно прочитать только один раз, чтобы понять что происходит. Далее внимание программиста переключается на таблицу с входными данными, которая полностью описывает поведение тестируемого кода.
Таблица получается заметно компактнее длинной простыни выше. В идеале она должна помещаться на один экран. И поскольку таким способом группируются похожие тесты, читать её очень легко.
Что думаете?