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

13

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

我有一个名为Foo的类,该类使用Bar类。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的可能选项:

  • 使用shared_ptr: 在这种情况下过于繁琐(Bar对象不在Foo和其他任何对象之间共享)
  • 通过IBar引用: 不是一个选择(Bar没有存储在Foo之外,所以创建的Bar对象将被销毁,使Foo保留悬挂引用)
  • 通过unique_ptr: 无法以此方式进行测试
  • 传值:不可能(同unique_ptr一样,将发生复制)。

我得到的唯一解决方案是在Foo成为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;
};

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


2
您可能会对阅读这个答案感兴趣。 - πάντα ῥεῖ
@πάντα ῥεῖ:感谢您提供的链接。我已经看过它了,对于需要unique_ptr作为参数的方法,它是有效的 - 但我不确定是否可以将此方法应用于构造函数。 - Quarra
3个回答

4

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

别名构造函数(shared_ptr)是什么作用?

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是由r管理的对象的成员或r.get()别名(例如下转换)。


4
赞成给 shared_ptr 添加别名构造函数,很好知道。如果我理解正确的话,使用别名构造函数创建的 shared_ptr 不管理传递的指针,因此它像普通的裸指针一样工作。或者说,使用别名构造函数有什么优势而不是使用裸指针吗? - Quarra

2
您可以在将模拟对象传递给构造函数之前保留对其的引用。我认为这会使代码有点脆弱,因为成员初始化顺序是固定的,但从语义上讲,它更加清晰。 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
}

这并没有解决生产代码中的问题:每当创建Foo时,都需要将IBar引用传递给它 - 这意味着无论谁管理Foo,都需要管理IBar,而我希望由Foo来管理IBar,以便Foo的用户可以只注入依赖项,而不必管理其生命周期。 - Quarra
我没有改变 Foo 的定义。它仍然接受并保留对 IBar 的唯一指针(即管理 IBar)。我只是在将其传递给 Foo 进行测试/模拟之前保留对 IBar 的引用。 - Rufus
你说得对 - 我的错,我没有注意到。很好的方法。 - Quarra

1

毕竟,我最终在各个地方都采用了这种方法:

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 协作良好。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接