依赖注入,unique_ptr模拟

Dependency injection with unique_ptr to mock

本文关键字:ptr 模拟 unique 注入 依赖      更新时间:2023-10-16

我有一个使用类 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: 在这种情况下太多了(Bar 对象在 Foo 之间不共享任何其他内容(
  • 通过引用 IBar: 不是一个选项(Bar 不存储在 Foo 之外,因此创建的 Bar 对象将被破坏,留下 Foo 悬空引用(
  • 按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;
};

难道没有更清洁的解决方案吗?我必须使用静态依赖注入(模板(吗?

实际上不是我建议在生产环境中使用的东西,但是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();
}

别名构造函数有什么作用?

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

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

在将模拟对象传递给构造函数之前,可以保留对模拟对象的引用。 我认为由于成员初始化排序,它使代码有点脆弱,但在语义上它的含义更清楚。 BarMock的所有权仍然只属于Foo,引用句柄由FooTest保留(类似于这个答案(。

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

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
}

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

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.