Мокинг производного класса boost с разделяемой памятью с помощью gtest - PullRequest
2 голосов
/ 09 июля 2020
• 1000

Теперь я хотел бы провести несколько тестов в своей программе, поэтому я sh имитирую функциональность моего объекта разделяемой памяти. Чтобы сделать это с помощью gtest, я создал базовый класс конфигурации, от которого будут производиться мой фиктивный класс и мой класс разделяемой памяти.

Для правильного использования gtest функции базового класса, которые я хотел бы to mock- должен быть виртуальным , но согласно документации boost , разделяемая память не может включать виртуальные функции , так что это своего рода тупик.

Пример моего базового класса:

class Configuration {
protected:
    YAML::Node YmlFile_;
public:
    struct System {
    private:
        float num1;
        float num2;
    public:
        virtual ~System(){}
        virtual float GetNum1() const {
            return num1;
        }

        virtual float GetNum2() const {
            return num2;
        }
    struct Logs{
    private:
        float num3;
        float num4;
    public:
        virtual ~Logs(){}
        virtual float GetNum3() const {
            return num3;
        }

        virtual float GetNum4() const {
            return num4;
        }
    Logs logs;
    System system;
    virtual System* GetSystem(){}
    virtual Logs* GetLogs(){}

Где на издеваемом классе я sh имитирую функции для получения структур (GetSystem, GetLogs), а затем имитирую их возвращаемые значения, но все еще имея возможность хранить «настоящий» производный класс конфигурации, который будет сохранен в общей памяти.

Есть идеи ..?

Ответы [ 2 ]

1 голос
/ 09 июля 2020

Сначала принципы:

Вам не нужно использовать виртуальные функции для имитации.

В местах, где вы не можете использовать runtime-polymorphi c типы, вы можете использовать stati c полиморфизм.

Но в этом случае еще лучше полностью отделить интерфейс конфигурации от реализации.

Реализуйте свой интерфейс, а не извлекайте из общей памяти контейнер ("источник конфигурации Is-A объект разделяемой памяти"). Вместо этого скажите «источник конфигурации имеет объект разделяемой памяти».

Другие важные вопросы:

  • Что делает YAML :: Node безопасным для разделяемой памяти? Скорее всего, это не так, потому что я не вижу указанного распределителя, и он, безусловно, включает в себя динамически выделяемую память, а также внутренние указатели.

    Я думаю, что этот подход может легко потерпеть неудачу только для это.

  • Если фактическим источником является YAML, почему бы просто не поделиться файлом вместо очень сложной общей памяти? (Мы здесь только касаемся поверхности. Мы даже не упомянули о синхронизации).

    Файловая система с незапамятных времен де-факто является «общей памятью» процессов на компьютере.

Пример интерфейса и реализации развязки

Интерфейсы делают так, что реализации могут быть развязаны, но, как вы заметили, наследование часто приводит к тому, что они не связаны 't.

Почему бы не написать что-то вроде:

struct ConfigData {
    struct System {
        float num1;
        float num2;

        struct Logs {
            float num3;
            float num4;
        } logs;
    } system;
};

Теперь создайте общий интерфейс (я упрощу его для демонстрации):

struct IConfiguration {
    virtual ConfigData const& getData() const = 0;
};

Итак у вас может быть либо серверная часть YAML:

class YAMLConfiguration : public IConfiguration {
  public:
    YAMLConfiguration(std::istream& is) : _node(YAML::Load(is)) {
        parse(_node, _data);
    }

    virtual ConfigData const& getData() const override {
        return _data;
    }
  private:
    YAML::Node _node;
    ConfigData _data;
};

, либо реализация с общей памятью:

#include <boost/interprocess/managed_shared_memory.hpp>
namespace bip = boost::interprocess;

class SharedConfiguration : public IConfiguration {
  public:
    SharedConfiguration(std::string name) 
        : _shm(bip::open_or_create, name.c_str(), 10ul << 10),
          _data(*_shm.find_or_construct<ConfigData>("ConfigData")())
    { }

    virtual ConfigData const& getData() const override {
        return _data;
    }
  private:
    bip::managed_shared_memory _shm;
    ConfigData& _data;
};

Полная демонстрация

Live On Coliru¹

struct ConfigData {
    struct System {
        float num1 = 77;
        float num2 = 88;

        struct Logs {
            float num3 = 99;
            float num4 = 1010;
        } logs;
    } system;
};

struct IConfiguration {
    virtual ConfigData const& getData() const = 0;
};

///////// YAML Backend
#include <yaml-cpp/yaml.h>
static bool parse(YAML::Node const& node, ConfigData::System::Logs& data) {
    data.num3 = node["num3"].as<float>();
    data.num4 = node["num4"].as<float>();
    return true;
}
static bool parse(YAML::Node const& node, ConfigData::System& data) {
    data.num1 = node["num1"].as<float>();
    data.num2 = node["num2"].as<float>();
    parse(node["Logs"], data.logs);
    return true;
}
static bool parse(YAML::Node const& node, ConfigData& data) {
    parse(node["System"], data.system);
    return true;
}

class YAMLConfiguration : public IConfiguration {
  public:
    YAMLConfiguration(std::istream& is) : _node(YAML::Load(is)) {
        parse(_node, _data);
    }

    virtual ConfigData const& getData() const override {
        return _data;
    }
  private:
    YAML::Node _node;
    ConfigData _data;
};

///////// Shared Memory Backend
#include <boost/interprocess/managed_shared_memory.hpp>
namespace bip = boost::interprocess;

class SharedConfiguration : public IConfiguration {
  public:
    SharedConfiguration(std::string name) 
        : _shm(bip::open_or_create, name.c_str(), 10ul << 10),
          _data(*_shm.find_or_construct<ConfigData>("ConfigData")())
    { }

    virtual ConfigData const& getData() const override {
        return _data;
    }
  private:
    bip::managed_shared_memory _shm;
    ConfigData& _data;
};


#include <iostream>
void FooFunction(IConfiguration const& cfg) {
    std::cout << "Logs.num3:" << cfg.getData().system.logs.num3 << "\n";
}

void FakeApplication() {
    std::cout << "Hello from FakeApplication\n";
    std::istringstream iss(R"(
System:
    num1: 0.1
    num2: 0.22
    Logs:
        num3: 0.333
        num4: 0.4444
    )");

    YAMLConfiguration config(iss);
    FooFunction(config);
}

void FakeTests() {
    std::cout << "Hello from FakeTests\n";
    SharedConfiguration config("shared_memory_name");
    FooFunction(config);
}

int main() {
    FakeApplication();
    FakeTests();
}

Печать

Hello from FakeApplication
Logs.num3:0.333
Hello from FakeTests
Logs.num3:99

Сводка и предостережение

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

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

  • см. эти мои ответы , чтобы увидеть, как это выглядит, и узнать, стоит ли оно того для вас

Также не забудьте о синхронизации между процессами, которые обращаются к общей памяти.

¹ Coliru не имеет yaml- cpp, но вы можете показать общую реализацию с помощью managed_mapped_file: Live On Coliru

0 голосов
/ 09 июля 2020

Похоже, что внедрение зависимостей может помочь здесь. Идея состоит в том, что вы не будете вводить объект Configuration ctor или каким-либо сеттером, но ваш тестируемый класс будет использовать шаблоны. В производстве он будет использовать Configuration, в тестах будет использовать ConfigurationStub. У этих двоих нет общего базового класса, но пока они имеют одинаковые сигнатуры методов - они отлично работают.

...