Сегодня у меня была дискуссия с коллегой о том, следует ли тестировать или не тестировать частных членов или личное государство в классе. Он почти убедил меня, почему это имеет смысл. Этот вопрос не предназначен для дублирования уже существующих вопросов StackOverflow о характере и причине тестирования частных членов, таких как: Что не так с созданием unit test друга класса, который он тестирует?
Предложение коллег было, на мой взгляд, немного хрупким, чтобы представить объявление друга в класс реализации unit test. По-моему, это не-go, потому что мы вводим некоторую зависимость тестируемого кода к тестовому коду, тогда как тестовый код уже зависит от проверенного кода = > циклической зависимости. Даже такие невинные вещи, как переименование тестового класса, приводят к нарушению единичных тестов и изменению кода в тестируемом коде.
Я хотел бы попросить гуру С++ судить по другому предложению, которое основано на том факте, что нам разрешено специализировать функцию шаблона. Представьте себе класс:
// tested_class.h
struct tested_class
{
tested_class(int i) : i_(i) {}
//some function which do complex things with i
// and sometimes return a result
private:
int i_;
};
Мне не нравится идея иметь геттер для i_, чтобы сделать его проверяемым. Поэтому мое предложение - это объявление шаблона функции test_backdoor в классе:
// tested_class.h
struct tested_class
{
explicit
tested_class(int i=0) : i_(i) {}
template<class Ctx>
static void test_backdoor(Ctx& ctx);
//some function which do complex things with i
// and sometimes return a result
private:
int i_;
};
Добавив только эту функцию, мы можем сделать проверяемые члены класса доступными. Обратите внимание: нет зависимости от классов unit test, а также от реализации функции шаблона. В этом примере реализация unit test использует среду тестирования Boost.
// tested_class_test.cpp
namespace
{
struct ctor_test_context
{
tested_class& tc_;
int expected_i;
};
}
// specialize the template member to do the rest of the test
template<>
void tested_class::test_backdoor<ctor_test_context>(ctor_test_context& ctx)
{
BOOST_REQUIRE_EQUAL(ctx.expected_i, tc_.i_);
}
BOOST_AUTO_TEST_CASE(tested_class_default_ctor)
{
tested_class tc;
ctor_test_context ctx = { tc, 0 };
tested_class::test_backdoor(ctx);
}
BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
tested_class tc(-5);
ctor_test_context ctx = { tc, -5 };
tested_class::test_backdoor(ctx);
}
Введя только одно объявление шаблона, которое вообще не является вызываемым, мы даем разработчику теста возможность переслать тестовую логику в функцию. Функция действует в безопасных контекстах типа и видна только внутри конкретного блока компиляции теста из-за анонимного характера тестового контекста. И самое лучшее, мы можем определить столько анонимных тестовых контекстов, сколько нам нравится, и специализировать тесты на них, не затрагивая тестируемый класс.
Конечно, пользователи должны знать, что такое специализация шаблонов, но действительно ли этот код плохой или странный или нечитаемый? Или я могу ожидать от разработчиков С++ знания о том, что такое спецификация шаблона С++ и как она работает?
Разрабатывая использование друга для объявления класса unit test, я не думаю, что это надежный. Представьте, что вы создаете фреймворк (или могут быть другие тестовые рамки). Он генерирует для каждого тестового примера отдельный тип. Но почему я должен заботиться, как долго могу писать:
BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
...
}
Если я использовал друзей, мне пришлось объявить каждый тестовый пример как друга тогда... Или в конечном итоге ввести некоторые тестовые функции в какой-то общий тип (например, fixture), объявить его как друга и перенаправить все тестовые вызовы на этот типа... Разве это не странно?
Я хотел бы, чтобы ваши профессионалы и против практики использовали этот подход.