Подтвердить что ты не робот

Тестирование частного члена класса на С++ без друга

Сегодня у меня была дискуссия с коллегой о том, следует ли тестировать или не тестировать частных членов или личное государство в классе. Он почти убедил меня, почему это имеет смысл. Этот вопрос не предназначен для дублирования уже существующих вопросов 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), объявить его как друга и перенаправить все тестовые вызовы на этот типа... Разве это не странно?

Я хотел бы, чтобы ваши профессионалы и против практики использовали этот подход.

4b9b3361

Ответ 1

То, что будет дальше, не является технически правильным ответом на ваш вопрос, поскольку он все равно будет использовать функциональность "друга" но он не требует модификации самого тестируемого объекта и я думаю, что это вызывает беспокойство по поводу нарушения инкапсуляции упомянутых в некоторых других ответах; он, хотя и требует написав какой-то шаблонный код.

Идея этого не моя, а реализация полностью основан на трюке, представленном и объясненном на нем блог (в сочетании с этим Sutter gotw чуть-чуть больше контекста, по крайней мере для меня) - вкратце CRTP, друзья, ADL и указатели на участников (Должен признаться, что к моему ужасу часть ADL я до сих пор не получить его полностью, но я постоянно работаю над его выяснением на 100%).

Я тестировал его с помощью компиляторов gcc 4.6, clang 3.1 и VS2010, и он отлично работает.

/* test_tag.h */
#ifndef TEST_TAG_H_INCLUDED_
#define TEST_TAG_H_INCLUDED_

template <typename Tag, typename Tag::type M>
struct Rob
{
    friend typename Tag::type get(Tag)
    {
        return M;
    }
};

template <typename Tag, typename Member> 
struct TagBase
{
    typedef Member type;
    friend type get(Tag);
};


#endif /* TEST_TAG_H_INCLUDED_ */

/* tested_class.h */
#ifndef TESTED_CLASS_H_INCLUDED_
#define TESTED_CLASS_H_INCLUDED_

#include <string>

struct tested_class
{
    tested_class(int i, const char* descr) : i_(i), descr_(descr) { }

private:
    int i_;
    std::string descr_;
};

/* with or without the macros or even in a different file */
#   ifdef TESTING_ENABLED
#   include "test_tag.h"

    struct tested_class_i : TagBase<tested_class_i, int tested_class::*> { };
    struct tested_class_descr : TagBase<tested_class_descr, const std::string tested_class::*> { };

    template struct Rob<tested_class_i, &tested_class::i_>;
    template struct Rob<tested_class_descr, &tested_class::descr_>;

#   endif

#endif /* TESTED_CLASS_H_INCLUDED_ */

/* test_access.cpp */
#include "tested_class.h"

#include <cstdlib>
#include <iostream>
#include <sstream>

#define STRINGIZE0(text) #text
#define STRINGIZE(text) STRINGIZE0(text)

int assert_handler(const char* expr, const char* theFile, int theLine)
{
    std::stringstream message;
    message << "Assertion " << expr << " failed in " << theFile << " at line " << theLine;
    message << "." << std::endl;
    std::cerr << message.str();

    return 1;
}

#define ASSERT_HALT() exit(__LINE__)

#define ASSERT_EQUALS(lhs, rhs) ((void)(!((lhs) == (rhs)) && assert_handler(STRINGIZE((lhs == rhs)), __FILE__, __LINE__) && (ASSERT_HALT(), 1)))

int main()
{
    tested_class foo(35, "Some foo!");

    // the bind pointer to member by object reference could
    // be further wrapped in some "nice" macros
    std::cout << " Class guts: " << foo.*get(tested_class_i()) << " - " << foo.*get(tested_class_descr()) << std::endl;
    ASSERT_EQUALS(35, foo.*get(tested_class_i()));
    ASSERT_EQUALS("Some foo!", foo.*get(tested_class_descr()));

    ASSERT_EQUALS(80, foo.*get(tested_class_i()));

    return 0; 
}

Ответ 2

Я думаю, что модульное тестирование - это тестирование наблюдаемого поведения тестируемого класса. Поэтому нет необходимости проверять частные детали, поскольку они сами не наблюдаются. То, как вы его тестируете, - это проверить, ведет ли объект себя так, как вы его ожидаете (что подразумевает, что все частные внутренние состояния в порядке).

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

Поэтому мой ответ не делает этого (даже если это технически возможно), поскольку это противоречит философии модульных тестов.

Ответ 3

Pros

  • Вы можете получить доступ к закрытым членам, чтобы проверить их.
  • Его довольно минимальное количество hack

против

  • Сломанная инкапсуляция
  • Сломанная инкапсуляция, которая является более сложной и такой же хрупкой, как friend
  • Смешивание теста с производственным кодом путем размещения test_backdoor на стороне производства
  • Проблема с обслуживанием (так же, как и при использовании тестового кода, вы создали очень плотную связь с вашим тестовым кодом)

Все плюсы и минусы в стороне, я думаю, вам лучше всего сделать некоторые архитектурные изменения, которые позволят лучше тестировать все сложные вещи.

Возможные решения

  • Используйте идиом Pimpl, поместите код complex в pimpl вместе с частным членом и напишите тест для Pimpl. Pimpl может быть объявлен как открытый член, разрешая внешнюю конкретизацию в unit test. Pimpl может состоять только из публичных пользователей, что упрощает тестирование
    • Недостаток: много кода
    • Недостаток: непрозрачный тип, который может быть труднее увидеть внутри отладки
  • Просто проверьте открытый/защищенный интерфейс класса. Проверьте контракт, который изложил ваш интерфейс.
    • Недостаток: модульные тесты сложны/невозможны для записи изолированным образом.
  • Подобно решениям Pimpl, но создайте в нем свободную функцию с кодом complex. Поместите объявление в закрытый заголовок (не являющийся частью общего интерфейса библиотек) и протестируйте его.
  • Прерывание инкапсуляции через друга методом тестирования/приспособлением
    • Возможные варианты: declare friend struct test_context;, поместите свой тестовый код внутри методов в реализацию struct test_context. Таким образом, вам не нужно соединять каждый тестовый пример, метод или приспособление. Это должно уменьшить вероятность того, что кто-то нарушит дружеские отношения.
  • Прерывание инкапсуляции через специализированную специализацию

Ответ 4

Обычно я не чувствую необходимости в unit test частных членах и функциях. Я бы предпочел ввести публичную функцию только для проверки правильного внутреннего состояния.

Но если я решаю задуматься над деталями, я использую неприятный быстрый хак в программе unit test:

#include <system-header>
#include <system-header>
// Include ALL system headers that test-class-header might include.
// Since this is an invasive unit test that is fiddling with internal detail
// that it probably should not, this is not a hardship.

#define private public
#include "test-class-header.hpp"
...

В Linux, по крайней мере, это работает, потому что имя языка С++ не включает частное/публичное состояние. Мне говорят, что в других системах это может быть неверно, и оно не будет связываться.

Ответ 5

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

// Public header
struct IFoo
{
public:
    virtual ~IFoo() { }
    virtual void DoSomething() = 0;
};
std::shared_ptr<IFoo> CreateFoo();

// Private test header
struct IFooInternal : public IFoo
{
public:
    virtual ~IFooInternal() { }
    virtual void DoSomethingPrivate() = 0;
};

// Implementation header
class Foo : public IFooInternal
{
public:
    virtual DoSomething();
    virtual void DoSomethingPrivate();
};

// Test code
std::shared_ptr<IFooInternal> p =
    std::dynamic_pointer_cast<IFooInternal>(CreateFoo());
p->DoSomethingPrivate();

Этот подход имеет явное преимущество в продвижении хорошего дизайна и не запутывании с объявлениями друзей. Конечно, вам больше не придется беспокоиться о проблемах, потому что возможность тестирования частных членов - это довольно нестандартное требование для начала.

Ответ 6

Я использовал функцию для проверки членов частного класса, которая была просто вызвана TestInvariant().

Он был частным членом класса и в режиме отладки вызывался в начале и в конце каждой функции (кроме начала ctor и конца dctor).

Он был виртуальным, и любой базовый класс назывался родительской версией, прежде чем он будет сам.

Это позволяло мне проверять внутреннее состояние класса все время, не подвергая всех членов класса кому-либо. У меня были очень простые тесты, но нет причин, по которым у вас не было бы сложных, или даже установить его с помощью флага и т.д.

Также вы можете иметь общедоступные тестовые функции, которые могут быть вызваны другими классами, которые вызывают функцию TestInvariant(). Поэтому, когда вам нужно изменить работу внутреннего класса, вам не нужно изменять какой-либо код пользователя.

Помогло бы это?

Ответ 7

Я думаю, первое, что нужно спросить: почему друг считается тем, что нужно использовать с осторожностью?

Потому что он прерывает инкапсуляцию. Он предоставляет другой класс или функцию с доступом к внутренностям вашего объекта, тем самым расширяя видимую область ваших частных членов. Если у вас много друзей, гораздо сложнее рассуждать о состоянии вашего объекта.

По моему мнению, решение шаблона в этом отношении хуже, чем у друга. Ваше основное заявленное преимущество шаблона заключается в том, что вам больше не нужно явно сопоставлять тест с классом. Я утверждаю, что, наоборот, это вред. Для этого есть две причины.

  • Тест связан с внутренними элементами вашего класса. Любой, кто меняет класс, должен знать, что, изменяя рядовых членов объекта, они могут нарушить тест. друг сообщает им, какие объекты могут быть связаны с внутренним состоянием вашего класса, но шаблонное решение не делает.

  • Друг ограничивает расширение ваших рядовых членов. Если вы знакомы с классом, вы знаете, что только этот класс может получить доступ к вашим внутренним лицам. Таким образом, если вы знакомы с тестом, вы знаете, что только тест может читать или записывать частные переменные-члены. Тем не менее, ваша задняя дверь шаблона может использоваться где угодно.

Решение шаблона неэффективно, поскольку оно скрывает проблему, а не фиксирует ее. Основная проблема с циклической зависимостью все еще существует: кто-то, изменяющий класс, должен знать о каждом использовании задней двери, и кто-то, меняющий тест, должен знать о классе. В основном, ссылка на тест из класса была удалена только путем передачи всех частных данных в общедоступные данные обходным путем.

Если вы должны получить доступ к закрытым членам вашего теста, просто прикрепите тестовое крепление и сделайте с ним. Это просто и понятно.

Ответ 8

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

#define private public

Это зло, но

  • не влияет на производственный код

  • не нарушает инкапсуляцию, поскольку пользователь/изменяющий уровень доступа

  • избегает тяжелого рефакторинга с идиомой PIMPL

чтобы вы могли пойти на это...

Ответ 9

Есть теория, что если она закрыта, ее не следует тестировать в одиночку, если это необходимо, тогда она должна быть переработана.

Для меня этот шиизм.

В некоторых проектах люди создают макрос для частных методов, например:

class Something{
   PRIVATE:
       int m_attr;
};

При компиляции для теста PRIVATE определяется как общедоступный, иначе он определяется как закрытый. это просто.