使用 unique_ptr 进行依赖注入以模拟
Dependency injection with unique_ptr to mock
我有一个使用 class Bar 的 class 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();
}
测试失败,因为 mock 对象被转移到 Foo,并且在这样的 mock 上设置期望失败。
DI的可能选项:
- by shared_ptr:在这种情况下太多了(Bar 对象不在 Foo 之间共享任何其他东西)
- 通过引用 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;
};
没有更简洁的解决方案吗?我必须使用静态依赖注入(模板)吗?
实际上我不会在生产环境中推荐这种方法,但是 别名构造函数 的 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 );
The aliasing constructor: constructs a shared_ptr
which shares ownership information with r
, but holds an unrelated and unmanaged pointer ptr
. Even if this shared_ptr
is the last of the group to go out of scope, it will call the destructor for the object originally managed by r
. However, calling get()
on this will always return a copy of ptr
. It is the responsibility of the programmer to make sure that this ptr
remains valid as long as this shared_ptr
exists, such as in the typical use cases where ptr
is a member of the object managed by r
or is an alias (e.g., downcast) of r.get()
毕竟,我最终到处都使用这种方法:
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
配合使用效果很好。
您可以在将模拟对象传递给构造函数之前保留对模拟对象的引用。我认为由于成员初始化顺序,它使代码有点脆弱,但它在语义上更清楚它的含义。 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
}
我有一个使用 class Bar 的 class 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();
}
测试失败,因为 mock 对象被转移到 Foo,并且在这样的 mock 上设置期望失败。
DI的可能选项:
- by shared_ptr:在这种情况下太多了(Bar 对象不在 Foo 之间共享任何其他东西)
- 通过引用 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;
};
没有更简洁的解决方案吗?我必须使用静态依赖注入(模板)吗?
实际上我不会在生产环境中推荐这种方法,但是 别名构造函数 的 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 );
The aliasing constructor: constructs a
shared_ptr
which shares ownership information withr
, but holds an unrelated and unmanaged pointerptr
. Even if thisshared_ptr
is the last of the group to go out of scope, it will call the destructor for the object originally managed byr
. However, callingget()
on this will always return a copy ofptr
. It is the responsibility of the programmer to make sure that thisptr
remains valid as long as thisshared_ptr
exists, such as in the typical use cases whereptr
is a member of the object managed byr
or is an alias (e.g., downcast) ofr.get()
毕竟,我最终到处都使用这种方法:
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
配合使用效果很好。
您可以在将模拟对象传递给构造函数之前保留对模拟对象的引用。我认为由于成员初始化顺序,它使代码有点脆弱,但它在语义上更清楚它的含义。 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
}