Каков наилучший способ тестирования частных методов с помощью 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

Пимпл идиома.

Подробнее: Google Test: Расширенное руководство.

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

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() несет ответственность за возвращение 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 (тестирование приватных функций через открытый интерфейс).

PS Вот соответствующая лекция о занятиях айсбергом: https://www.youtube.com/watch?v=4cVZvoFGJTU

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

Другие вопросы по тегам