本文内容
第16章 使用Mock组件进行单元测试
第16章 使用Mock组件进行单元测试
注意:本章的源代码可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch16_unittests_mock
使用 Mock 进行测试
第 15 章 为组件编写单元测试 编写了一个涉及两个实际组件的单元测试。另一种常见模式是编写一个测试,该测试涉及一个 real 组件和一个模拟另一个 mock 组件。在这种情况下,实际组件将是 OscillatorComponent,而 mock 将是 MockTransformComponent。
此类测试的目的是检查 OscillatorComponent 是否调用了特定的 EBus 调用。这与检查某个变量是否具有预期值不同。相反,这种类型的单元测试检查是否调用了特定接口。因此,我们编写一个单元测试,检查 OscillatorComponent 是否在类似 Transform 的组件上调用 SetWorldTranslation。
“mock” 组件是伪测试组件。创建它是为了测试 mock 和一些真实组件之间的交互。它是一个强大的测试工具。O3DE 附带了 Google Mock,它提供了一个强大的 C++ 模拟框架。
模拟 TransformComponent
从概念上讲,O3DE 中的组件由它实现的事件总线处理程序标识。你可以选择让 mock 对象继承 TransformComponent 实现的所有此类处理程序。但是,在 OscillatorComponent 与 TransformComponent 交互的情况下,我们知道只涉及 TransformBus,因此只处理 TransformBus::Handler 就足够了。
#include <AzCore/Component/TransformBus.h>
...
/**
* \brief Pretend to be a TransformComponent
*/
class MockTransformComponent
: public AZ::Component
, public AZ::TransformBus::Handler
{
当然,它仍然必须是一个 O3DE 组件,所以它必须继承自 AZ::Component 并实现最基本的组件接口。
...
...
// be sure this guid is unique, avoid copy-paste errors!
AZ_COMPONENT(MockTransformComponent, "{7E8087BD-46DA-4708-ADB0-08D7812CA49F}");
// Just a mock object, no reflection necessary
static void Reflect(ReflectContext*) {}
// Mocking out pure virtual methods
void Activate() override
{
AZ::TransformBus::Handler::BusConnect(GetEntityId());
}
void Deactivate() override
{
AZ::TransformBus::Handler::BusDisconnect();
}
由于 MockTransformComponent 需要处理 TransformBus 接口,因此它必须连接到总线。
模拟虚拟方法
现在我们来看看 Google Mock 的第一个功能:模拟虚拟方法。
class MockTransformComponent
...
// OscillatorComponent will be calling these methods
MOCK_METHOD1(SetWorldTranslation, void (const AZ::Vector3&));
MOCK_METHOD0(GetWorldTranslation, AZ::Vector3 ());
...
这些将覆盖 TransformBus 的虚拟方法:
class TransformInterface {
...
virtual void SetWorldTranslation(const AZ::Vector3&);
virtual AZ::Vector3 GetWorldTranslation()
MOCK_METHOD0 , MOCK_METHOD1 等为这些方法提供了许多附加功能的存根实现。在本章中,将使用 SetWorldTranslation 和 GetWorldTranslation。TransformBus 中的其他一些纯虚拟方法则不会。这些仍然需要被模拟出来,这样我们才能实例化这个对象:
// Unused methods but they are pure virtual in TransformBus
MOCK_METHOD0(IsStaticTransform, bool ());
MOCK_METHOD0(IsPositionInterpolated, bool ());
MOCK_METHOD0(IsRotationInterpolated, bool ());
MOCK_METHOD0(GetLocalTM, const Transform& ());
MOCK_METHOD0(GetWorldTM, const Transform& ());
MOCK_METHOD1(BindTransformChangedEventHandler, void(TransformChangedEvent::Handler&));
MOCK_METHOD1(BindParentChangedEventHandler, void(ParentChangedEvent::Handler&));
MOCK_METHOD1(BindChildChangedEventHandler, void(ChildChangedEvent::Handler&));
MOCK_METHOD2(NotifyChildChangedEvent, void(ChildChangeType, EntityId));
所以整个 mock 定义如下:
例 16.1.MockTransformComponent 定义
/**
* \brief Pretend to be a TransformComponent
*/
class MockTransformComponent
:
public AZ::Component
,
public AZ::TransformBus::Handler
{
public:
// be sure this guid is unique, avoid copy-paste errors!
AZ_COMPONENT(MockTransformComponent,
"{7E8087BD-46DA-4708-ADB0-08D7812CA49F}");
// Just a mock object, no reflection necessary
static void Reflect(ReflectContext*) {}
// Mocking out pure virtual methods
void Activate() override
{
AZ::TransformBus::Handler::BusConnect(GetEntityId());
}
void Deactivate() override
{
AZ::TransformBus::Handler::BusDisconnect();
}
// OscillatorComponent will be calling these methods
MOCK_METHOD1(SetWorldTranslation, void (const AZ::Vector3&));
MOCK_METHOD0(GetWorldTranslation, AZ::Vector3 ());
// Unused methods but they are pure virtual in TransformBus
MOCK_METHOD0(IsStaticTransform, bool ());
MOCK_METHOD0(IsPositionInterpolated, bool ());
MOCK_METHOD0(IsRotationInterpolated, bool ());
MOCK_METHOD0(GetLocalTM, const Transform& ());
MOCK_METHOD0(GetWorldTM, const Transform& ());
MOCK_METHOD1(BindTransformChangedEventHandler,
void(TransformChangedEvent::Handler&));
MOCK_METHOD1(BindParentChangedEventHandler,
void(ParentChangedEvent::Handler&));
MOCK_METHOD1(BindChildChangedEventHandler,
void(ChildChangedEvent::Handler&));
MOCK_METHOD2(NotifyChildChangedEvent,
void(ChildChangeType, EntityId));
};
使用 Mock 组件设置 Entity
与上一章非常相似,我们将创建一个可重用的 test fixture 来处理内存分配器和组件的注册。
class OscillatorMockTest
: public ::UnitTest::AllocatorsFixture
{
AZStd::unique_ptr<AZ::SerializeContext> m_sc;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_md;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_od;
protected:
void SetUp() override
{
::UnitTest::AllocatorsFixture::SetUp();
// register components involved in testing
m_sc = AZStd::make_unique<AZ::SerializeContext>();
m_md.reset(MockTransformComponent::CreateDescriptor());
m_md->Reflect(m_sc.get());
m_od.reset(OscillatorComponent::CreateDescriptor());
m_od->Reflect(m_sc.get());
}
...
区别在于我们如何将组件附加到实体。具体来说,我们需要保留一个指向 MockTransformComponent 的指针,这样我们就可以在测试体中对它执行一些 Google Mock 魔法作。
...
};
// helper method
void PopulateEntity(Entity& e)
{
// OscillatorComponent is the component we are testing
e.CreateComponent<OscillatorComponent>();
// We can mock out Transform and test the interaction
mock = new MockTransformComponent();
e.AddComponent(mock);
// Bring the entity online
e.Init();
e.Activate();
}
MockTransformComponent* mock = nullptr;
现在,我们有一个 helper 方法来创建和激活实体,同时保留指向 mock 的指针。
注意:e.AddComponent(mock) 将组件实例的内存所有权传递给实体,因此不要尝试删除测试体中的 mock 指针。实体析构函数将处理该问题。
设置对接口调用的期望
现在我们准备好编写测试体了。它从准备实体开始。
TEST_F(OscillatorMockTest, Calls_SetWorldTranslation)
{
Entity e;
PopulateEntity(e);
我们将在本章中使用的一个功能是设置对调用特定方法的期望。例如,我们将测试 SetWorldTranslation 是否被调用了一次。
// expect SetWorldTranslation() to be called
EXPECT_CALL(*mock, SetWorldTranslation(_)).Times(1);
注意:SetWorldTranslation() 中的下划线是一个特殊的 Google Mock 对象 ::test ing:: 匹配任何参数。因此,此语句为:“预期使用任何参数调用 SetWorldTranslation 一次。
回想一下,OscillatorComponent 调用 GetWorldTranslation 来获取当前位置。我们在 MockTransformComponent 中将其模拟为:
MOCK_METHOD0(GetWorldTranslation, AZ::Vector3 ());
所以默认情况下它不会返回任何值。如果您的测试在没有任何额外准备的情况下运行并调用此方法,它将在运行时出错并显示错误:
[ RUN ] OscillatorMockTest.Calls_SetWorldTranslation
unknown file: error: C++ exception with description
"Uninteresting mock function call - returning default value.
Function call: GetWorldTranslation()
The mock function has no default action set, and its return
type has no default value set." thrown in the test body.
Google Mock 提供了一种自定义 GetWorldTranslation 行为的方法,而无需修改 MockTransformComponent。以下是我们如何将其设置为返回 (0,0,0) 的向量:
// setup a return value for GetWorldTranslation()
ON_CALL(*mock, GetWorldTranslation()).WillByDefault(
Return(AZ::Vector3::CreateZero()));
在简单的英语中,这意味着“每当调用 GetWorldTranslation 时返回 Vector3::CreateZero”。
有了这个,测试体就简短而甜蜜了:
TEST_F(OscillatorMockTest, Calls_SetWorldTranslation)
{
Entity e;
PopulateEntity(e);
// setup a return value for GetWorldTranslation()
ON_CALL(*mock, GetWorldTranslation()).WillByDefault(
Return(AZ::Vector3::CreateZero()));
// expect SetWorldTranslation() to be called
EXPECT_CALL(*mock, SetWorldTranslation(_)).Times(1);
TickBus::Broadcast(&TickBus::Events::OnTick, 0.1f,
ScriptTimePoint());
}
它执行以下作:
- 为测试准备实体。
- 设置 GetWorldTranslation 的默认返回值。
- 设置对 TransformBus 上的 OscillatorComponent 调用的期望值。
- 滴答一次。
- 在范围结束时,Google Mock 将为我们完成工作,以确保达到预期。
提示:如果您要运行此类测试,您会看到 Google Mock 的警告:
GMOCK WARNING: Uninteresting mock function call - taking default action specified MyProject\Gem\Code\Tests\MyProjectTest.cpp(105): Function call: GetWorldTranslation() Returns: 16-byte object <00-00 ... 00-00> NOTE: You can safely ignore the above warning unless this call should not happen. Do not suppress it by blindly adding an EXPECT_CALL() if you don't mean to enforce the call.
Google Mock 在此警告中告诉我们,GetWorldTranslation 的调用方式与我们预期的 OscillatorComponent 相同,但测试主体没有对此指定任何期望。正如警告所暗示的那样,一个好的经验法则是只测试真正重要的东西。在这种情况下,SetWorldTranslation 是重要的调用,并且已设置了对它的期望。
一旦你确定测试正常,你可以通过将创建时 mock 对象的类型从以下更改为:
// We can mock out Transform and test the interaction mock = new MockTransformComponent(); e.AddComponent(mock);
到:
// We can mock out Transform and test the interaction mock = new NiceMock<MockTransformComponent>(); e.AddComponent(mock);
NiceMock 是一个 Google Mock 类,它静默地忽略对 mock 对象的意外调用。
小结
注意:本章的源代码可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch16_unittests_mock
如果一切顺利,您应该会看到本章单元测试的以下输出:
[----------] 1 test from OscillatorMockTest
[ RUN ] OscillatorMockTest.Calls_SetWorldTranslation
[ OK ] OscillatorMockTest.Calls_SetWorldTranslation (2 ms)
[----------] 1 test from OscillatorMockTest (3 ms total)
为了完整起见,这是完整的源代码。
例 16.2.OscillatorMockTest.cpp
#include <OscillatorComponent.h>
#include <AzCore/Component/ComponentApplication.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzTest/AzTest.h>
using namespace AZ;
using namespace AZStd;
using namespace MyProject;
using namespace ::testing;
/**
* \brief Pretend to be a TransformComponent
*/
class MockTransformComponent
:
public AZ::Component
,
public AZ::TransformBus::Handler
{
public:
// be sure this guid is unique, avoid copy-paste errors!
AZ_COMPONENT(MockTransformComponent,
"{7E8087BD-46DA-4708-ADB0-08D7812CA49F}");
// Just a mock object, no reflection necessary
static void Reflect(ReflectContext*) {}
// Mimic Transform component service
static void GetProvidedServices(
AZ::ComponentDescriptor::DependencyArrayType& req)
{
}
req.push_back(AZ_CRC("TransformService"));
// Mocking out pure virtual methods
void Activate() override
{
AZ::TransformBus::Handler::BusConnect(GetEntityId());
}
void Deactivate() override
{
AZ::TransformBus::Handler::BusDisconnect();
}
// OscillatorComponent will be calling these methods
MOCK_METHOD1(SetWorldTranslation, void (const AZ::Vector3&));
MOCK_METHOD0(GetWorldTranslation, AZ::Vector3 ());
// Unused methods but they are pure virtual in TransformBus
MOCK_METHOD0(IsStaticTransform, bool ());
MOCK_METHOD0(IsPositionInterpolated, bool ());
MOCK_METHOD0(IsRotationInterpolated, bool ());
MOCK_METHOD0(GetLocalTM, const Transform& ());
MOCK_METHOD0(GetWorldTM, const Transform& ());
MOCK_METHOD1(BindTransformChangedEventHandler,
void(TransformChangedEvent::Handler&));
MOCK_METHOD1(BindParentChangedEventHandler,
void(ParentChangedEvent::Handler&));
MOCK_METHOD1(BindChildChangedEventHandler,
void(ChildChangedEvent::Handler&));
MOCK_METHOD2(NotifyChildChangedEvent,
void(ChildChangeType, EntityId));
};
class OscillatorMockTest
:
public ::UnitTest::AllocatorsFixture
{
AZStd::unique_ptr<AZ::SerializeContext> m_sc;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_md;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_od;
protected:
void SetUp() override
{
::UnitTest::AllocatorsFixture::SetUp();
// register components involved in testing
m_sc = AZStd::make_unique<AZ::SerializeContext>();
m_md.reset(MockTransformComponent::CreateDescriptor());
m_md->Reflect(m_sc.get());
m_od.reset(OscillatorComponent::CreateDescriptor());
m_od->Reflect(m_sc.get());
}
void TearDown() override
{
m_md.reset();
m_od.reset();
m_sc.reset();
::UnitTest::AllocatorsFixture::TearDown();
}
// helper method
void PopulateEntity(Entity& e)
{
// OscillatorComponent is the component we are testing
e.CreateComponent<OscillatorComponent>();
// We can mock out Transform and test the interaction
mock = new NiceMock<MockTransformComponent>();
e.AddComponent(mock);
// Bring the entity online
e.Init();
e.Activate();
}
MockTransformComponent* mock = nullptr;
};
TEST_F(OscillatorMockTest, Calls_SetWorldTranslation)
{
Entity e;
PopulateEntity(e);
// setup a return value for GetWorldTranslation()
ON_CALL(*mock, GetWorldTranslation()).WillByDefault(
Return(AZ::Vector3::CreateZero()));
// expect SetWorldTranslation() to be called
EXPECT_CALL(*mock, SetWorldTranslation(_)).Times(1);
TickBus::Broadcast(&TickBus::Events::OnTick, 0.1f,
ScriptTimePoint());
}