Каков наилучший способ тестирования частных методов с помощью GoogleTest?

Я хочу проверить некоторые частные методы с помощью GoogleTest.

class Foo
{
private:
    int bar(...)
}

GoogleTest позволяет несколько способов сделать это.

ВАРИАНТ 1

С FRIEND_TEST:

class Foo
{
private:
    FRIEND_TEST(Foo, barReturnsZero);
    int bar(...);
}

TEST(Foo, barReturnsZero)
{
    Foo foo;
    EXPECT_EQ(foo.bar(...), 0);
}

Это подразумевает включение "gtest/gtest.h" в файл исходного файла.

ВАРИАНТ 2

Объявить тестовое устройство как друга для класса и определить аксессоры в приборе:

class Foo
{
    friend class FooTest;
private:
    int bar(...);
}

class FooTest : public ::testing::Test
{
protected:
    int bar(...) { foo.bar(...); }
private:
    Foo foo;
}

TEST_F(FooTest, barReturnsZero)
{
    EXPECT_EQ(bar(...), 0);
}

ВАРИАНТ 3

Идиома Pimpl.

Подробнее: https://github.com/google/googletest/blob/master/googletest/docs/AdvancedGuide.md#private-class-members

Есть ли другие способы тестирования частных методов? Каковы некоторые плюсы и минусы каждого варианта?

Спасибо!

Ответ 1

Есть как минимум еще два варианта. Я перечислил некоторые другие варианты, которые вы должны рассмотреть, объяснив определенную ситуацию.

Вариант 4:

Рассмотрим рефакторинг кода, чтобы часть, которую вы хотите протестировать, была общедоступна в другом классе. Обычно, когда вы испытываете соблазн проверить частный метод класса, это признак плохого дизайна. Один из самых распространенных (анти) патернов, который я вижу, это то, что Майкл Перс называет классом "Айсберг". Классы "Айсберг" имеют один общедоступный метод, а остальные - частные (вот почему он соблазн проверить частные методы). Это может выглядеть примерно так:

RuleEvaluator (украден из Майкла Перья)

Например, вы можете протестировать GetNextToken(), вызвав его последовательно, и увидев, что он возвращает ожидаемый результат. Подобная функция гарантирует тест: это поведение не является тривиальным, особенно если ваши правила токенинга сложны. Позвольте сделать вид, что это не все, что сложно, и мы просто хотим привязать маркеры, ограниченные пространством. Итак, вы пишете тест, возможно, он выглядит примерно так:

TEST(RuleEvaluator, canParseSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar"
    RuleEvaluator re = RuleEvaluator(input_string);
    EXPECT_EQ(re.GetNextToken(), "1");
    EXPECT_EQ(re.GetNextToken(), "2");
    EXPECT_EQ(re.GetNextToken(), "test");
    EXPECT_EQ(re.GetNextToken(), "bar");
    EXPECT_EQ(re.HasMoreTokens(), false);
}

Хорошо, это выглядит довольно красиво. Мы хотим, чтобы мы поддерживали это поведение по мере внесения изменений. Но GetNextToken() является частной функцией! Поэтому мы не можем это проверить, потому что он даже не компилируется. Но как насчет изменения класса RuleEvaluator для соблюдения принципа единой ответственности (принцип единой ответственности)? Например, у нас, похоже, есть парсер, токенизатор и оценщик, помеченные в один класс. Разве не лучше было бы просто отделить эти обязанности? Кроме того, если вы создаете класс Tokenizer, тогда его общедоступные методы будут HasMoreTokens() и GetNextTokens(). Класс RuleEvaluator может иметь объект Tokenizer в качестве члена. Теперь мы можем сохранить те же тесты, что и выше, за исключением того, что мы тестируем класс Tokenizer вместо класса RuleEvaluator.

Вот что это могло бы выглядеть в UML:

Реализованный класс RuleEvaluator

Обратите внимание, что этот новый дизайн увеличивает модульность, поэтому вы можете повторно использовать эти классы в других частях вашей системы (прежде чем вы не смогли, частные методы не могут быть повторно использованы по определению). Это главное преимущество взлома RuleEvaluator, а также повышенная понятность/локальность.

Тест будет выглядеть очень похожим, за исключением того, что он действительно будет компилироваться на этот раз, поскольку метод GetNextToken() теперь доступен в классе Tokenizer:

TEST(Tokenizer, canParseSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar"
    Tokenizer tokenizer = Tokenizer(input_string);
    EXPECT_EQ(tokenizer.GetNextToken(), "1");
    EXPECT_EQ(tokenizer.GetNextToken(), "2");
    EXPECT_EQ(tokenizer.GetNextToken(), "test");
    EXPECT_EQ(tokenizer.GetNextToken(), "bar");
    EXPECT_EQ(tokenizer.HasMoreTokens(), false);
}

Вариант 5

Просто не проверяйте частные функции. Иногда их не стоит тестировать, потому что они будут протестированы через открытый интерфейс. Много раз я вижу тесты, которые выглядят очень похожими, но проверяют две разные функции/методы. Что происходит, так это то, что при изменении требований (и они всегда это делают) у вас теперь есть 2 сломанных теста вместо 1. И если вы действительно протестировали все свои частные методы, у вас может быть больше 10 сломанных тестов вместо 1. Короче говоря, тестирование частных функций (с помощью FRIEND_TEST или их публикации), которые в противном случае могли быть протестированы через открытый интерфейс, вызывают дублирование теста. Вы действительно этого не хотите, потому что ничего больного больше, чем ваш набор тестов замедляет вас. Он должен уменьшить время разработки и снизить затраты на обслуживание! Если вы тестируете частные методы, которые в противном случае проверяются через открытый интерфейс, набор тестов может очень хорошо сделать наоборот и активно увеличивать затраты на обслуживание и увеличивать время разработки. Когда вы делаете публичную публичную функцию или используете что-то вроде FRIEND_TEST, вы обычно будете сожалеть об этом.

Рассмотрим следующую возможную реализацию класса Tokenizer:

Возможный смысл Tokenizer

Скажем, что SplitUpByDelimiter() отвечает за возврат a std::vector<std::string>, так что каждый элемент в векторе является токеном. Кроме того, пусть просто скажем, что GetNextToken() является просто итератором над этим вектором. Поэтому ваши тесты могут выглядеть так:

TEST(Tokenizer, canParseSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar"
    Tokenizer tokenizer = Tokenizer(input_string);
    EXPECT_EQ(tokenizer.GetNextToken(), "1");
    EXPECT_EQ(tokenizer.GetNextToken(), "2");
    EXPECT_EQ(tokenizer.GetNextToken(), "test");
    EXPECT_EQ(tokenizer.GetNextToken(), "bar");
    EXPECT_EQ(tokenizer.HasMoreTokens(), false);
}

// Pretend we have some class for a FRIEND_TEST
TEST_F(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar"
    Tokenizer tokenizer = Tokenizer(input_string);
    std::vector<std::string> result = tokenizer.SplitUpByDelimiter(" ");
    EXPECT_EQ(result.size(), 4);
    EXPECT_EQ(result[0], "1");
    EXPECT_EQ(result[1], "2");
    EXPECT_EQ(result[2], "test");
    EXPECT_EQ(result[3], "bar");
}

Ну, теперь позвольте сказать, что требования меняются, и теперь вы должны анализировать "," вместо пространства. Естественно, вы ожидаете, что один тест сломается, но боль усиливается при проверке частных функций. IMO, проверка google не должна допускать FRIEND_TEST. Это почти никогда не то, что вы хотите сделать. Майкл Перс ссылается на такие вещи, как FRIEND_TEST как на "инструмент для нащупывания", поскольку он пытается коснуться других частных частей.

Я рекомендую избегать опций 1 и 2, когда это возможно, поскольку это обычно вызывает "дублирование теста", и, как следствие, во время изменения требований ломается много больше тестов, чем необходимо. Используйте их в качестве последнего средства. Варианты 1 и 2 - это самые быстрые способы "проверки частных методов" здесь и сейчас (как в самой быстрой реализации), но в конечном итоге они действительно повредят производительности.

PIMPL тоже может иметь смысл, но он по-прежнему позволяет создать довольно плохой дизайн. Будьте осторожны с ним.

Я бы рекомендовал вариант 4 (реорганизация на более мелкие тестируемые компоненты) в качестве правильного места для запуска, но иногда вам действительно нужен вариант 5 (тестирование частных функций через открытый интерфейс).

P.S. Здесь соответствующая лекция об классах айсберга: https://www.youtube.com/watch?v=4cVZvoFGJTU

P.S.S. Что касается всего в программном обеспечении, ответ зависит от этого. Нет ни одного размера. Вариант, который решает вашу проблему, будет зависеть от ваших конкретных обстоятельств.