使用unique_ptr进行依赖注入以进行模拟

时间:2016-11-09 13:21:06

标签: c++ unit-testing dependency-injection gmock

我有一个使用类Bar的类Foo。 Bar仅在Foo中使用,而Foo正在管理Bar,因此我使用unique_ptr(不是引用,因为我不需要在Foo之外使用Bar):

using namespace std;
struct IBar {
    virtual ~IBar() = default;  
    virtual void DoSth() = 0;
};

struct Bar : public IBar {
    void DoSth() override { cout <<"Bar is doing sth" << endl;};    
};

struct Foo {
  Foo(unique_ptr<IBar> bar) : bar_(std::move(bar)) {}

  void DoIt() {
    bar_->DoSth();
  }
private:
  unique_ptr<IBar> bar_;
};

到目前为止一切顺利,这很好。但是,当我想对代码进行单元测试时,我遇到了问题:

namespace {
struct BarMock : public IBar {
  MOCK_METHOD0(DoSth, void());
};
}

struct FooTest : public Test {
  FooTest() : barMock{ make_unique<BarMock>() }, out(std::move(barMock)) {}

  unique_ptr<BarMock> barMock;
  Foo out;
};

TEST_F(FooTest, shouldDoItWhenDoSth) {
  EXPECT_CALL(*barMock, DoSth());

  out.DoIt();
}

测试失败,因为模拟对象被转移到Foo,并且对这样的模拟设置期望失败。

DI的可能选项:

  • by shared_ptr:在这种情况下太多了(在Foo之间没有任何其他东西共享Bar对象)
  • 通过引用IBar:不是一个选项(Bar不存储在Foo之外,因此创建的Bar对象将被破坏,留下Foo的悬空引用)
  • by unique_ptr:以呈现的方式无法测试
  • 通过传递值:不可能(复制将发生 - 与unique_ptr相同的问题)。

我得到的唯一解决方案是在Foo成为BarMock的所有者之前将原始指针存储到BarMock,即:

struct FooTest : public Test {
  FooTest() : barMock{new BarMock} {
    auto ptr = unique_ptr<BarMock>(barMock);
    out.reset(new Foo(std::move(ptr)));
  }

  BarMock* barMock;
  unique_ptr<Foo> out;
};

是否有更清洁的解决方案?我是否必须使用静态依赖注入(模板)?

3 个答案:

答案 0 :(得分:3)

实际上我不会在生产环境中推荐这样的东西,但shared_ptr别名构造函数可能代表了一个肮脏且工作正常的解决方案。
一个最小的工作示例(不使用 gtest ,抱歉,我来自移动应用程序,无法直接测试):

#include<memory>
#include<iostream>
#include<utility>

struct IBar {
    virtual ~IBar() = default;  
    virtual void DoSth() = 0;
};

struct Bar : public IBar {
    void DoSth() override { std::cout <<"Bar is doing sth" << std::endl;};    
};

struct Foo {
    Foo(std::unique_ptr<IBar> bar) : bar(std::move(bar)) {}

    void DoIt() {
        bar->DoSth();
    }
private:
    std::unique_ptr<IBar> bar;
};

int main() {
    std::unique_ptr<Bar> bar = std::make_unique<Bar>();
    std::shared_ptr<Bar> shared{std::shared_ptr<Bar>{}, bar.get()};
    Foo foo{std::move(bar)};
    shared->DoSth();
    foo.DoIt();
}

我猜你的测试会变成这样:

struct BarMock: public IBar {
    MOCK_METHOD0(DoSth, void());
};

struct FooTest : public testing::Test {
    FooTest() {
        std::unique_ptr<BarMock> bar = std::make_unique<BarMock>();
        barMock = std::shared_ptr<BarMock>{std::shared_ptr<BarMock>{}, bar.get()};
        out = std::make_unique<Foo>{std::move(bar)};
    }

    std::shared_ptr<BarMock> barMock;
    std::unique_ptr<Foo> out;
};

TEST_F(FooTest, shouldDoItWhenDoSth) {
    EXPECT_CALL(*barMock, DoSth());
    out->DoIt();
}

aliasing constructor做了什么?

template< class Y > 
shared_ptr( const shared_ptr<Y>& r, element_type *ptr );
  

别名构造函数:构造一个shared_ptr,它与r共享所有权信息,但保存一个不相关且非托管的指针ptr。即使这个shared_ptr是超出范围的最后一个组,它也会调用最初由r管理的对象的析构函数。但是,在此处调用get()将始终返回ptr的副本。只要ptr存在,程序员就有责任确保此shared_ptr仍然有效,例如在ptr是对象管理对象的典型用例中rr.get()别名(例如,向下转发)

答案 1 :(得分:1)

您可以保留对模拟对象的引用,然后再将其传递给构造函数。由于成员初始化的顺序,我认为这会使代码有些脆弱,但是从语义上讲它更清楚。 BarMock的所有权仍仅属于Foo,其引用句柄由FooTest(类似于this answer)保留。

与答案基本相同,但使用引用而不是原始指针

class FooTest : public ::testing::Test
{
    protected:
        FooTest() :
            bar_mock_ptr(std::make_unique<BarMock>()),
            bar_mock(*bar_mock_ptr),
            foo(std::move(bar_mock_ptr))
        {}
    private:
        // This must be declared before bar_mock due to how member initialization is ordered
        std::unique_ptr<BarMock> bar_mock_ptr; // moved and should not be used anymore
    protected:
        BarMock& bar_mock;
        Foo foo; //ensure foo has the same lifetime as bar_mock
}

答案 2 :(得分:0)

毕竟,我最终在所有地方都使用了这种方法:

struct FooTest : public Test {
  FooTest() : barMock{new BarMock} {
    auto ptr = unique_ptr<BarMock>(barMock);
    out.reset(new Foo(std::move(ptr)));
  }

  BarMock* barMock;
  unique_ptr<Foo> out;
};

,它可以与gtest/gmock一起正常工作。