本文内容
第15章 为组件编写单元测试
第15章 为组件编写单元测试
首先应使系统运行,即使它除了调用适当的虚拟子程序集外,没有任何用处。然后,它被一点一点地充实,子程序反过来被开发为作或对下一级别中空存根的调用。 —Frederick P. Brooks Jr. “没有灵丹妙药 - 软件工程中的本质和意外”,1986 年
介绍
注意:本章的源代码可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch15_unittests
在游戏开发中,一个经常被忽视的开发过程是单元测试。O3DE 引擎附带强大的 Google Test 和 Google Mock 框架。
提示:
本章将向您展示如何为 OscillatorComponent 编写一个简单的单元测试,我们在第 9 章 使用 AZ::TickBus 中实现。
单元测试生成目标
每当在 O3DE 中创建新 Gem 时,您都会获得一个默认的单元测试构建目标,例如 MyGem.Tests。
在 Visual Studio 解决方案中,您可以将其与其他生成目标一起找到。 但是,当我们生成 MyProject 时,它没有收到单元测试构建目标。我们需要 MyProject.Tests 构建目标来测试 OscillatorComponent,因为该组件位于 MyProject 中,而不是 Gem 中。虽然完全没有必要在项目中放置组件,但它将作为从头开始添加单元测试所需的一个很好的示例
图 15.1.单元测试在 Gem 中的位置
由于我们要向项目添加新的构建目标,因此我们必须修改 C:\git\book\MyProject\Code\CMakeLists.txt。这是要添加到CMakeLists.txt末尾的更改。
例 15.1. 添加 MyProject.Tests构建目标
# See if globally, tests are supported
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
ly_add_target(
NAME MyProject.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE Gem
FILES_CMAKE
myproject_test_files.cmake
INCLUDE_DIRECTORIES
PRIVATE
Tests
Source
BUILD_DEPENDENCIES
PRIVATE
AZ::AzTest
AZ::AzFramework
Gem::MyProject.Static
)
# Add MyProject.Tests to googletest
ly_add_googletest(
NAME Gem::MyProject.Tests
)
endif()
这里的重要元素是:
- 将 MyProject.Tests 目标标记为单元测试构建目标。
ly_add_googletest(
NAME Gem::MyProject.Tests
)
这样,您就可以从 Visual Studio 将此目标作为 Google Unittest 启动。
- 针对 MyProject.Static 的 MyProject.Tests 目标链接,后者具有带有 OscillatorComponent 的对象代码。
BUILD_DEPENDENCIES
PRIVATE
Gem::MyProject.Static
- myproject_test_files.cmake 包含要为单元测试构建的文件。 例 15.2.myproject_test_files.cmake
set(FILES
Tests/MyProjectTest.cpp
Tests/OscillatorTests.cpp
)
图 15.2.单元测试在 Gem 中的位置
单元测试
MyProjectTest.cpp将用作单元测试设置
例 15.3. MyProjectTest.cpp
#include <AzTest/AzTest.h>
AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV);
运行单元测试
在我们开始修改单元测试之前,让我们先看看如何运行这些测试。在 Visual Studio 中,您可以通过右键单击项目并选择 Set as StartUp Project 来将 MyProject.Tests 设置为启动项目。运行项目将执行单元测试。
例 15.4.单元测试输出
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from OscillatorTest
[ RUN ] OscillatorTest.EntityMovingUp
[ OK ] OscillatorTest.EntityMovingUp (7 ms)
[----------] 1 test from OscillatorTest (8 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (9 ms total)
[ PASSED ] 1 test.
提示:如果您在 MyProject.Tests 的属性页中占有一席之地,则可以找到从命令行运行单元测试所需的步骤:
C:/O3DE/21.11.2/bin/Windows/profile/Default/AzTestRunner.exe C:/git/book/build/bin/profile/MyProject.Tests.dll AzRunUnitTests
单元测试的结构
注意:本章的代码可以在 GitHub 上找到,网址为: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch15_unittests
O3DE 中游戏的大部分功能都是在组件中完成的。所以它只允许为组件编写单元测试。常见的方法如下:
- 创建一个 AZ::Entity。
- 添加您正在测试的组件。
- 添加您正在测试的交互中涉及的另一个组件。
- 调用触发或推进交互的 API(如 EBuses)。
- 测试结果。
例如,以下是我们将为 OscillatorComponent 编写的单元测试的结构(来自第 9 章,使用 AZ::TickBus):
- 如前所述创建 AZ::Entity。这不需要特别的。您可以在堆栈或堆上创建一个。
- 将 OscillatorComponent 添加到实体中。
- 添加 AzFramework::TransformComponent。
- 调用 TickBus,因为 OscillatorComponent 在 OnTick 方法中移动了实体。
- 测试 TransformComponent 是否在一次更新后向上移动。 我将介绍单元测试源代码文件的每个部分,并在最后显示整个代码。
内存分配器设置
O3DE 单元测试的难点是正确分配内存。您可以通过使用 AzCore/UnitTest/TestTypes.h 中的预制单元测试基类::UnitTest::AllocatorsFixture 来避免这种麻烦。它具有 SetUp 和 TearDown 虚拟方法,它从 GoogleTest 基类::testing::Test 中覆盖这些方法。
#include <AzCore/UnitTest/TestTypes.h>
...
class OscillatorTest
:
public ::UnitTest::AllocatorsFixture
{
...
...
void SetUp() override
{
::UnitTest::AllocatorsFixture::SetUp();
这样,O3DE 内存分配器将得到正确设置,您无需处理错误,例如:
'SystemAllocator' NOT ready for use! Call Create first!
如果您曾经尝试编写自己的 O3DE 单元测试,则可能已经看到过此类运行时崩溃。AllocatorsFixture 解决了这个问题。
注册组件
单元测试在精简环境中运行。没有加载 Gem,即使项目的组件也不会自动注册。
提示:如果您尝试在单元测试中创建组件,但未先向 O3DE 注册该组件,则会触发运行时断言,例如: o3de\Code\Framework\AzCore\AzCore/UnitTest/UnitTest.h(155): error: Entity ‘6688814788’ [6688814788] cannot be activated. No descriptor registered for Component class ‘OscillatorComponent’.
这可以通过创建 AZ::SerializeContext 的实例并反映您希望使用的组件来解决。
AZStd::unique_ptr<AZ::SerializeContext> m_sc;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_td;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_od;
...
// register components involved in testing
m_sc = AZStd::make_unique<AZ::SerializeContext>();
m_td.reset(TransformComponent::CreateDescriptor());
m_td->Reflect(m_sc.get());
m_od.reset(OscillatorComponent::CreateDescriptor());
m_od->Reflect(m_sc.get());
向实体添加组件
创建实体后,非常简单:
Entity e;
您可以添加我们之前注册的组件:
...
...
// helper method
void PopulateEntity(Entity& e)
{
// OscillatorComponent is the component we are testing
e.CreateComponent<OscillatorComponent>();
// And how it interacts with
e.CreateComponent<AzFramework::TransformComponent>();
激活实体
回想一下,在调用 Activate 方法之前,组件是非活动状态的。您可以通过初始化和激活实体来执行此作。
// Bring the entity online
e.Init();
e.Activate();
测试体
如前所述,此测试将触发一个 tick 事件,并在 TransformComponent 上测试结果。
TEST_F(OscillatorTest, EntityMovingUp)
{
Entity e;
PopulateEntity(e);
// Move entity to (0,0,0)
TransformBus::Event(e.GetId(),
&TransformBus::Events::SetWorldTranslation,
Vector3::CreateZero());
// tick once
TickBus::Broadcast(&TickBus::Events::OnTick, 0.1f,
ScriptTimePoint());
// Get entity's position
Vector3 change;
TransformBus::EventResult(change, e.GetId(),
&TransformBus::Events::GetWorldTranslation);
// check that it moved up, by any amount
ASSERT_TRUE(change.GetZ() > 0);
}
注意:OscillatorTest 是此测试体继承自的单元测试装置。这就是我们可以直接使用 PopulateEntity 的原因。EntityMovingUp 是测试的名称。
单元测试钩子
O3DE 有一个包装 GoogleTest 的宏。您只需从其中一个测试文件中调用它一次。
#include <AzTest/AzTest.h>
AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV);
输出
如果一切顺利,您应该在运行单元测试时在命令行中看到以下输出。
[----------] 1 test from OscillatorTest
[ RUN ] OscillatorTest.EntityMovingUp
[ OK ] OscillatorTest.EntityMovingUp (1 ms)
[----------] 1 test from OscillatorTest (2 ms total)
源代码
注意:本章的源代码可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch15_unittests
例 15.5. MyProjectTest.cpp
#include <AzTest/AzTest.h>
AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV);
例 15.6. OscillatorTest.cpp
#include <OscillatorComponent.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Components/TransformComponent.h>
#include <AzTest/AzTest.h>
namespace MyProject
{
class OscillatorTest
:
public ::UnitTest::AllocatorsFixture
{
AZStd::unique_ptr<AZ::SerializeContext> m_sc;
AZStd::unique_ptr<AZ::ComponentDescriptor> m_td;
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_td.reset(AzFramework::TransformComponent
::CreateDescriptor());
m_td->Reflect(m_sc.get());
m_od.reset(OscillatorComponent::CreateDescriptor());
m_od->Reflect(m_sc.get());
}
void TearDown() override
{
m_td.reset();
m_od.reset();
m_sc.reset();
::UnitTest::AllocatorsFixture::TearDown();
}
// helper method
void PopulateEntity(AZ::Entity& e)
{
// OscillatorComponent is the component we are testing
e.CreateComponent<OscillatorComponent>();
// And how it interacts with
e.CreateComponent<AzFramework::TransformComponent>();
// Bring the entity online
e.Init();
e.Activate();
}
};
TEST_F(OscillatorTest, EntityMovingUp)
{
using namespace AZ;
Entity e;
PopulateEntity(e);
// Move entity to (0,0,0)
TransformBus::Event(e.GetId(),
&TransformBus::Events::SetWorldTranslation,
Vector3::CreateZero());
// tick once
TickBus::Broadcast(&TickBus::Events::OnTick, 0.1f,
ScriptTimePoint());
// Get entity's position
Vector3 change = AZ::Vector3::CreateZero();
TransformBus::EventResult(change, e.GetId(),
&TransformBus::Events::GetWorldTranslation);
// check that it moved up, by any amount
ASSERT_TRUE(change.GetZ() > 0);
}
}