Mocking a boost shared memory derived class with gtest

强颜欢笑 提交于 2021-01-28 02:54:34

问题


I have a simple CPP class storing some configuration of my project.

This class is stored using boost interprocess shared memory, and so can be accessed from different processes running on my server.

Now, I would like to run some tests on my program- so I wish to mock the functionality of my shared-memory-object. In order to do that with gtest, I've created a base configuration class, which my mock class and my shared memory class will derive from.

In order to use gtest correctly, the base's class functions I would like to mock- must be virtual, but according to boost documentation, the shared memory can't include virtual functions, so it's kind of a deadlock.

Example of my Base class:

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(){}

Where on the mocked class I wish to mock the functions to get the structs (GetSystem, GetLogs) and then mock their return values, but still having the ability to holds a 'real' derived class of Configuration that will be saved in the shared memory.

Any ideas..?


回答1:


The principles first:

You don't have to use virtual functions to mock.

In places where you can't use runtime-polymorphic types you can use static polymorphism.

But in this case it seems better yet, decouple the configuration interface from the implementation altogether.

Implement your interface not deriving from a shared memory container ("configuration source Is-A shared memory object"). Instead say "configuration source Has-A shared memory object".

Other critical questions:

  • What makes YAML::Node safe for shared memory? Likely it isn't, because I don't see an allocator specified, and it most certainly involves dynamically allocated memory, as well as internal pointers.

    I think the approach could easily be dead in the water just for that.

  • If the actual source is YAML, why not just share the file, instead of highly complicated shared memory? (We're only grazing the surface here. We did't even mention synchronization).

    The filesystem is the de-facto "shared memory" of processes on a computer, since the beginning of time.

Example Decoupling Interface And Implementation

Interfaces make it so that implementations can be decoupled, but as you noticed, inheritance often makes it so that they aren't.

Why not write something like:

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

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

Now make a shared interface (I'll simplify it for the demo):

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

So you can have either your YAML backend:

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;
};

Or a shared-memory implementation:

#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;
};

Full Demo

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();
}

Prints

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

Summary And Caution

In short, think thrice before using the shared memory. It's not as simple as you think.

In all likelihood, some of your config values will be something else than POD data types (you know, maybe a string) and suddenly you'll have to care about allocators:

  • see these answers of mine to see what that looks like and to see whether it's worth it for you purposes

Also, don't forget about synchronization between processes that access shared memory.


¹ Coliru doesn't have yaml-cpp, but you can show the shared implementation with managed_mapped_file: Live On Coliru




回答2:


Seems like hi-perf dependency injection could do the trick here. The idea is that you won't be injecting the Configuration object by ctor or some setter, but your class-under-test will be using templates. In production, it will use Configuration, in tests it will use ConfigurationStub. These two doesn't share a base class, but as long as have the same methods' signatures - it works just fine.



来源:https://stackoverflow.com/questions/62809697/mocking-a-boost-shared-memory-derived-class-with-gtest

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!