【问题标题】:Dependency injection with unique_ptr to mock使用 unique_ptr 进行模拟的依赖注入
【发布时间】:2017-03-23 07:49:36
【问题描述】:

我有一个使用类 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;
};

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

【问题讨论】:

  • 您可能有兴趣阅读this answer
  • @πάντα ῥεῖ:谢谢你的链接。我已经看过它,它适用于将 unique_ptr 作为参数的方法 - 但我不确定是否可以将这种方法应用于构造函数。

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


【解决方案1】:

实际上我不建议在生产环境中使用,但 shared_ptraliasing constructor 可能代表您的情况的肮脏且有效的解决方案。
一个最小的工作示例(不使用 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 );

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

【讨论】:

  • +1 用于 shared_ptr 的别名构造函数,很高兴知道。如果我理解正确,则使用别名构造函数创建的 shared_ptr 不会管理传递的 ptr,因此它可以用作常规原始指针。或者使用它而不是原始 ptr 有什么好处?
【解决方案2】:

毕竟,我最终到处都使用这种方法:

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

【讨论】:

    【解决方案3】:

    您可以在将模拟对象传递给构造函数之前保留对它的引用。我认为由于成员初始化排序,它使代码有点脆弱,但在语义上它的含义更清楚。 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
    }
    

    【讨论】:

    • 这并不能解决生产代码中的问题:每当创建Foo 时,都需要将IBar 引用传递给它——这意味着无论谁管理Foo,都需要管理@ 987654329@ 而我想让IBarFoo 管理,以便Foo 的用户可以注入依赖项,但不必管理它的生命周期。
    • 我不会更改Foo 的定义。它仍在获取并保留指向IBar 的唯一指针(即管理IBar)。我只是保留对IBar 的引用,然后将其传递给Foo 以进行测试/模拟
    • 你是对的 - 我的错,我没有注意到。不错的方法。
    猜你喜欢
    • 1970-01-01
    • 2016-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-26
    • 2014-01-19
    相关资源
    最近更新 更多