Mocking out classes for testing in Lumberyard is no different than mocking anything using google mocks
For the simple case, I suggest reading their Google Mocks for Dummies section.
Creating the mock
Recently I have been mocking out the process for running test that involve performing raycasting, so I’m going to share a bit of that process here. This involved mocking out the IPhysicalWorld interface, found in CryCommon/physinterface.h, which is the interface one goes through currently to do raycast.
Here is the final mocked out class:
#include <gmock/gmock.h> #include <gtest/gtest.h> #include <physinterface.h> namespace Principia { namespace Mocks { struct IPhysicalWorldMock : public IPhysicalWorld { MOCK_METHOD0(Init, void()); MOCK_METHOD1(Shutdown, void(int bDeleteGeometries)); MOCK_METHOD0(Release, void()); MOCK_METHOD0(GetGeomManager, IGeomManager*()); MOCK_METHOD0(GetPhysUtils, IPhysUtils*()); MOCK_METHOD8(SetupEntityGrid, void(int axisz, Vec3 org, int nx, int ny, float stepx, float stepy, int log2PODscale, int bCyclic)); MOCK_METHOD0(Cleanup, void()); MOCK_METHOD1(RegisterBBoxInPODGrid, void(const Vec3* BBox)); MOCK_METHOD1(UnregisterBBoxInPODGrid, void(const Vec3* BBox)); MOCK_METHOD0(DeactivateOnDemandGrid, void()); MOCK_METHOD2(AddRefEntInPODGrid, int(IPhysicalEntity* pent, const Vec3* BBox)); MOCK_METHOD3(SetHeightfieldData, IPhysicalEntity*(const primitives::heightfield* phf, int* pMatMapping, int nMats)); MOCK_METHOD1(GetHeightfieldData, IPhysicalEntity*(primitives::heightfield* phf)); MOCK_METHOD2(SetHeightfieldMatMapping, void(int* pMatMapping, int nMats)); MOCK_METHOD0(GetPhysVars, PhysicsVars*()); MOCK_METHOD6(CreatePhysicalEntity, IPhysicalEntity*(pe_type type, pe_params* params, PhysicsForeignData pForeignData, int iforeigndata, int id1, IGeneralMemoryHeap* pHeapULL)); MOCK_METHOD8(CreatePhysicalEntity, IPhysicalEntity*(pe_type type, float lifeTime, pe_params* params, PhysicsForeignData pForeignData, int iForeignData, int id1, IPhysicalEntity* pHostPlaceholder, IGeneralMemoryHeap* pHeapULL)); MOCK_METHOD5(CreatePhysicalPlaceholder, IPhysicalEntity*(pe_type type, pe_params* params, PhysicsForeignData pForeignData, int iForeignData, int id1)); MOCK_METHOD3(DestroyPhysicalEntity, int(IPhysicalEntity* pent, int mode, int bThreadSafe)); MOCK_METHOD4(SetPhysicalEntityId, int(IPhysicalEntity* pent, int id, int bReplace, int bThreadSafe)); MOCK_METHOD1(GetPhysicalEntityId, int(IPhysicalEntity* pent)); MOCK_METHOD1(GetPhysicalEntityById, IPhysicalEntity*(int id)); MOCK_METHOD4(SetSurfaceParameters, int(int surface_idx, float bounciness, float friction, unsigned int flags)); MOCK_METHOD4(GetSurfaceParameters, int(int surface_idx, float& bounciness, float& friction, unsigned int& flags)); MOCK_METHOD8(SetSurfaceParameters, int(int surface_idx, float bounciness, float friction, float damage_reduction, float ric_angle, float ric_dam_reduction, float ric_vel_reduction, unsigned int flags)); MOCK_METHOD8(GetSurfaceParameters, int(int surface_idx, float& bounciness, float& friction, float& damage_reduction, float& ric_angle, float& ric_dam_reduction, float& ric_vel_reduction, unsigned int& flags)); MOCK_METHOD2(TimeStep, void(float time_interval, int flagsnt_all)); MOCK_METHOD0(GetPhysicsTime, float()); MOCK_METHOD0(GetiPhysicsTime, int()); MOCK_METHOD1(SetPhysicsTime, void(float time)); MOCK_METHOD1(SetiPhysicsTime, void(int itime)); MOCK_METHOD2(SetSnapshotTime, void(float time_snapshot, int iType)); MOCK_METHOD2(SetiSnapshotTime, void(int itime_snapshot, int iType)); MOCK_METHOD5(GetEntitiesInBox, int(Vec3 ptmin, Vec3 ptmax, IPhysicalEntity**& pList, int objtypes, int szListPrealloc)); MOCK_METHOD0(GetMaxThreads, int()); MOCK_METHOD3(RayWorldIntersection, int(const SRWIParams& rp, const char* pNameTag, int iCaller)); MOCK_METHOD1(TracePendingRays, int(int bDoActualTracing)); MOCK_METHOD0(ResetDynamicEntities, void()); MOCK_METHOD0(DestroyDynamicEntities, void()); MOCK_METHOD0(PurgeDeletedEntities, void()); MOCK_METHOD1(GetEntityCount, int(int iEntType)); MOCK_METHOD1(ReserveEntityCount, int(int nExtraEnts)); MOCK_METHOD0(GetEntitiesIterator, IPhysicalEntityIt*()); MOCK_METHOD5(SimulateExplosion, void(pe_explosion* pexpl, IPhysicalEntity** pSkipEnts, int nSkipEnts, int iTypesnt_rigid, int iCaller)); MOCK_METHOD7(RasterizeEntities, void(const primitives::grid3d& grid, uchar* rbuf, int objtypes, float massThreshold, const Vec3& offsBBox, const Vec3& sizeBBox, int flags)); MOCK_METHOD5(DeformPhysicalEntity, int(IPhysicalEntity* pent, const Vec3& ptHit, const Vec3& dirHit, float r, int flags)); MOCK_METHOD1(UpdateDeformingEntities, void(float time_interval)); MOCK_METHOD2(CalculateExplosionExposure, float(pe_explosion* pexpl, IPhysicalEntity* pient)); MOCK_METHOD2(IsAffectedByExplosion, float(IPhysicalEntity* pent, Vec3* impulse)); MOCK_METHOD4(AddExplosionShape, int(IGeometry* pGeom, float size, int idmat, float probability)); MOCK_METHOD1(RemoveExplosionShape, void(int id)); MOCK_METHOD1(RemoveAllExplosionShapes, void(void (*OnRemoveGeom)(IGeometry* pGeom))); MOCK_METHOD2(DrawPhysicsHelperInformation, void(IPhysRenderer* pRenderer, int iCaller)); MOCK_METHOD3(DrawEntityHelperInformation, void(IPhysRenderer* pRenderer, int iEntityId, int iDrawHelpers)); MOCK_METHOD5(CollideEntityWithBeam, int(IPhysicalEntity* _pent, Vec3 org, Vec3 dir, float r, ray_hit* phit)); MOCK_METHOD6(CollideEntityWithPrimitive, int(IPhysicalEntity* _pent, int itype, primitives::primitive* pprim, Vec3 dir, ray_hit* phit, intersection_params* pip)); MOCK_METHOD6(RayTraceEntity, int(IPhysicalEntity* pient, Vec3 origin, Vec3 dir, ray_hit* pHit, pe_params_pos* pp, unsigned int geomFlagsAnyeom_colltype0)); MOCK_METHOD3(PrimitiveWorldIntersection, float(const SPWIParams& pp, WriteLockCond* pLockContacts, const char* pNameTagWI_NAME_TAG)); MOCK_METHOD1(GetMemoryStatistics, void(ICrySizer* pSizer)); MOCK_METHOD1(SetPhysicsStreamer, void(IPhysicsStreamer* pStreamer)); MOCK_METHOD1(SetPhysicsEventClient, void(IPhysicsEventClient* pEventClient)); MOCK_METHOD1(GetLastEntityUpdateTime, float(IPhysicalEntity* pent)); MOCK_METHOD1(GetEntityProfileInfo, int(phys_profile_info*& pList)); MOCK_METHOD1(GetFuncProfileInfo, int(phys_profile_info*& pList)); MOCK_METHOD1(GetGroupProfileInfo, int(phys_profile_info*& pList)); MOCK_METHOD1(GetJobProfileInfo, int(phys_job_info*& pList)); MOCK_METHOD4(AddEventClient, void(int type, int (*func)(const EventPhys*), int bLogged, float priority)); MOCK_METHOD3(RemoveEventClient, int(int type, int (*func)(const EventPhys*), int bLogged)); MOCK_METHOD0(PumpLoggedEvents, void()); MOCK_METHOD0(GetPumpLoggedEventsTicks, uint32()); MOCK_METHOD0(ClearLoggedEvents, void()); MOCK_METHOD0(AddGlobalArea, IPhysicalEntity*()); IPhysicalEntity* AddArea(Vec3* pt, int npt, float zmin, float zmax, const Vec3& pos = Vec3(0, 0, 0), const quaternionf& q = quaternionf(IDENTITY), float scale = 1.0f, const Vec3& normal = Vec3(ZERO), int* pTessIdx = 0, int nTessTris = 0, Vec3* pFlows = 0) { return nullptr; } MOCK_METHOD4(AddArea, IPhysicalEntity*(IGeometry* pGeom, const Vec3& pos, const quaternionf& q, float scale)); MOCK_METHOD6(AddArea, IPhysicalEntity*(Vec3* pt, int npt, float r, const Vec3& posec3, const quaternionf& quaternionf, float scale)); MOCK_METHOD1(GetNextArea, IPhysicalEntity*(IPhysicalEntity* pPrevArea)); MOCK_METHOD8(CheckAreas, int(const Vec3& ptc, Vec3& gravity, pe_params_buoyancy* pb, int nMaxBuoys, int iMedium1, const Vec3& vecec3, IPhysicalEntity* pent, int iCaller)); MOCK_METHOD1(SetWaterMat, void(int imat)); MOCK_METHOD0(GetWaterMat, int()); MOCK_METHOD1(SetWaterManagerParams, int(pe_params* params)); MOCK_METHOD1(GetWaterManagerParams, int(pe_params* params)); MOCK_METHOD1(GetWatermanStatus, int(pe_status* status)); MOCK_METHOD0(DestroyWaterManager, void()); volatile int* GetInternalLock(int idx) { return nullptr; } MOCK_METHOD2(SerializeWorld, int(const char* fname, int bSave)); MOCK_METHOD2(SerializeGeometries, int(const char* fname, int bSave)); MOCK_METHOD3(SerializeGarbageTypedSnapshot, void(TSerialize ser, int iSnapshotType, int flags)); MOCK_METHOD2(SavePhysicalEntityPtr, void(TSerialize ser, IPhysicalEntity* pent)); MOCK_METHOD1(LoadPhysicalEntityPtr, IPhysicalEntity*(TSerialize ser)); MOCK_METHOD3(GetEntityMassAndCom, void(IPhysicalEntity* pIEnt, float& mass, Vec3& com)); MOCK_METHOD2(AddDeferredEvent, EventPhys*(int type, EventPhys* event)); }; } }
As you can see, the IPhysicsWorld interface had a lot of functions to mock. There is a tool to automagically generate these mocked classes for you, but I found it to be more trouble then it’s worth for huge classes. The problem is that hunting down the problems it generates takes just as long as doing it by hand.
You’ll also notice that I didn’t mock a few functions. These functions merely call other existing functions that are mocked. They are helpers that pack parameters into parameter structs. It’s perfectly okay for our mock to include their implementation and leave them unmocked.
Also AddArea
has more than 10 parameters and google mock only supports 10 parameters at most. So my current hope is that I don’t need that function in my test :P. If I did, I would need to do some extra work to manually track if it was called.
Using the mock
So the method that I call in my code is
inline int RayWorldIntersection(const Vec3& org, const Vec3& dir, int objtypes, unsigned int flags, ray_hit* hits, int nMaxHits, IPhysicalEntity** pSkipEnts = 0, int nSkipEnts = 0, PhysicsForeignData pForeignData = 0, int iForeignData = 0, const char* pNameTag = RWI_NAME_TAG, ray_hit_cached* phitLast = 0, int iCaller = GetMaxPhysThreads())
This is one of the methods we don’t mock because internally it just calls the following, which we do mock:
int RayWorldIntersection(const SRWIParams& rp, const char* pNameTag = RWI_NAME_TAG, int iCaller = GetMaxPhysThreads())
This is the function that we have mocked, and we should expect my code to call in our test.
Now, there is a bit of setup in getting a proper test in Lumberyard which I’ll go into in another tutorial. For now, lets focus on the use of the mock. I will post the full code including my setup at the end.
Here is how I started our test:
TEST_F(UnitTestRaycastFixture, CallBasicRaycast) { auto hit = std::make_unique<ray_hit>(); IPhysicalWorld::SRWIParams rp; rp.org = Vec3{0, 1, 0}; rp.dir = Vec3{0, 1000, 0}; rp.objtypes = ent_all; rp.flags = rwi_stop_at_pierceable | rwi_colltype_any; rp.hits = hit.get(); rp.nMaxHits = 1; rp.pForeignData = 0; rp.iForeignData = 0; rp.phitLast = 0; rp.pSkipEnts = 0; rp.nSkipEnts = 0; EXPECT_CALL(*systemMock, GetViewCamera()).Times(1); EXPECT_CALL(*physicalWorldMock, RayWorldIntersection(rp, RWI_NAME_TAG, 0)).Times(1); CameraRaycastRequest cameraRaycastRequest; cameraRaycastRequest.PerformRayCast(); }
When you use an EXPECT_CALL
, you are telling google test that you expect a certain function to be called on one of your mock object, and in this case I’m saying it should be called exactly 1 time.
systemMock
is another mock that I initialized in the fixture from #include "Mocks/ISystemMock.h"
which Lumberyard provides. The mocked function in the systemMock that my code calls is GetViewCamera()
, so I’m testing to ensure it is called as expected.
The PerformRayCast()
is the entry point for the code that I wrote that we need to make this test for.
When I tried to compile this, I got a very long compiler error basically telling me that it doesn’t know how to compare IPhysicalWorld::SRWIParams
to know if the “actual” meets the “expected” value (ie, no operator== for it). So to remedy this, I had to create the operator:
bool operator==(IPhysicalWorld::SRWIParams left, IPhysicalWorld::SRWIParams right) { auto result = left.org == right.org && left.dir == right.dir && left.objtypes == right.objtypes && left.flags == right.flags && left.nMaxHits == right.nMaxHits; return result; }
This is especially good because we can specify exactly what we want to compare to determine if we get the correct values from our own code. For instance, I don’t really care about phitLast, pSkipEnts, etc. So this lets us change our test slightly.
bool operator==(IPhysicalWorld::SRWIParams left, IPhysicalWorld::SRWIParams right) { auto result = left.org == right.org && left.dir == right.dir && left.objtypes == right.objtypes && left.flags == right.flags && left.nMaxHits == right.nMaxHits; return result; } TEST_F(UnitTestRaycastFixture, CallBasicRaycast) { auto hit = std::make_unique<ray_hit>(); IPhysicalWorld::SRWIParams rp; rp.org = Vec3{0, 1, 0}; rp.dir = Vec3{0, 1000, 0}; rp.objtypes = ent_all; rp.flags = rwi_stop_at_pierceable | rwi_colltype_any; rp.nMaxHits = 1; EXPECT_CALL(*systemMock, GetViewCamera()).Times(1); EXPECT_CALL(*physicalWorldMock, RayWorldIntersection(rp, RWI_NAME_TAG, 0)).Times(1); CameraRaycastRequest cameraRaycastRequest; cameraRaycastRequest.PerformRayCast(); }
Now this succeeds, however, we get a warning in our test output that looks like this:
[----------] 1 test from UnitTestRaycastFixture [ RUN ] UnitTestRaycastFixture.CallBasicRaycast GMOCK WARNING: Uninteresting mock function call - returning default value. Function call: GetMaxThreads() Returns: 0 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. See https://github.com/google/googletest/blob/master/googlemock/docs/CookBook.md#knowing-when-to-expect for details. [ OK ] UnitTestRaycastFixture.CallBasicRaycast (15 ms) [----------] 1 test from UnitTestRaycastFixture (16 ms total)
This is not telling us that our test is uninteresting, but rather that our test is calling a mocked function that we did not tell it to expect. In this case, it is the GetMaxThreads()
function. This is correct because it does indeed call this function and we forgot to tell our test to expect that.
So we add that to our expected calls:
EXPECT_CALL(*systemMock, GetViewCamera()).Times(1); EXPECT_CALL(*physicalWorldMock, RayWorldIntersection(rp, RWI_NAME_TAG, 0)).Times(1); EXPECT_CALL(*physicalWorldMock, GetMaxThreads()).Times(1);
Now our test runs perfectly. We have verified that my code calls the RayWorldIntersection
function as expected and passes all the correct values.
The full Setup
As promised, here is the entire setup to get this test up and running. I’ll give a few comments on it, but this post is focusing on the google mocking framework, not the google test framework.
#include "StdAfx.h" #pragma warning( push ) #pragma warning(disable: 4800) // 'int' : forcing value to bool 'true' or 'false' (performance warning) #include "Tests/Mocks/IPhysicallWorldMock.h" #include <Mocks/ISystemMock.h> #pragma warning( pop ) #include "Buses/CameraRaycastRequest.h" #include <AzTest/AzTest.h> #include <Tests/TestTypes.h> #include <gmock/gmock.h> #include <AzCore/std/smart_ptr/unique_ptr.h> using namespace Principia; using namespace UnitTest; using ::testing::ReturnRef; class UnitTestRaycast : public AllocatorsFixture { protected: UnitTestRaycast(): AllocatorsFixture(15, false), cry_camera(CCamera()), globalEnv(SSystemGlobalEnvironment()) { } void SetUp() override { gEnv = &globalEnv; ON_CALL(*systemMock, GetViewCamera()).WillByDefault(ReturnRef(cry_camera)); gEnv->pSystem = systemMock.get(); gEnv->pPhysicalWorld = physicalWorldMock.get(); } void TearDown() override { } SSystemGlobalEnvironment globalEnv; CCamera cry_camera; AZStd::unique_ptr<Mocks::IPhysicalWorldMock> physicalWorldMock = AZStd::make_unique<Mocks::IPhysicalWorldMock>(); AZStd::unique_ptr<SystemMock> systemMock = AZStd::make_unique<SystemMock>(); }; bool operator==(IPhysicalWorld::SRWIParams left, IPhysicalWorld::SRWIParams right) { auto result = left.org == right.org && left.dir == right.dir && left.objtypes == right.objtypes && left.flags == right.flags && left.nMaxHits == right.nMaxHits; return result; } TEST_F(UnitTestRaycast, CallBasicRaycast) { auto hit = std::make_unique<ray_hit>(); IPhysicalWorld::SRWIParams rp; rp.org = Vec3{0, 1, 0}; rp.dir = Vec3{0, 1000, 0}; rp.objtypes = ent_all; rp.flags = rwi_stop_at_pierceable | rwi_colltype_any; rp.nMaxHits = 1; EXPECT_CALL(*systemMock, GetViewCamera()).Times(1); EXPECT_CALL(*physicalWorldMock, RayWorldIntersection(rp, RWI_NAME_TAG, 0)).Times(1); EXPECT_CALL(*physicalWorldMock, GetMaxThreads()).Times(1); CameraRaycastRequest cameraRaycastRequest; cameraRaycastRequest.PerformRayCast(); }
In the Setup()
you will notice some interesting things.
ON_CALL(*systemMock, GetViewCamera()).WillByDefault(ReturnRef(cry_camera));
What is the world is this?!.. you may ask. This was hours of headache. Massive Ibuprofen was involved here.
Apparently, when you call a function and it returns an integral type or some common type, google mock can handle that. However, when you return a custom type, you have to tell google mocks what the default value is for that type. So in this case, it was enough to merely instantiate the CCamera type and pass it in as the default return type for GetViewCamera()
. If it were a normal pointer or value, we could use Return() but since this function returns a reference, we need to use ReturnRef().
gEnv = &globalEnv; gEnv->pSystem = systemMock.get(); gEnv->pPhysicalWorld = physicalWorldMock.get();
If you’ve used anything from the CryEngine directly, then you have probably dealt with the gEnv. My code that does the raycast is still using the old cry way, so I needed to mock these out.
Setting this up is pretty simple. Fortunately, SSystemGlobalEnvironment globalEnv
is just a pointer-bag and has no functionality, so we didn’t need to mock that.
I used unique pointers for the mock classes so I don’t have to deal with deleting them later, but otherwise you instantiate them like anything else.
#pragma warning( push ) #pragma warning(disable: 4800) // 'int' : forcing value to bool 'true' or 'false' (performance warning) #include "Tests/Mocks/IPhysicallWorldMock.h" #include <Mocks/ISystemMock.h> #pragma warning( pop )
HERETIC! HEATHEN! I’m ignoring warnings!!!
Yeah, but Lumberyard started it… so…
When mocking a volatile int*, the compiler gives a warning because somewhere internally in google mocks, it is comparing it as a bool, which it says can have performance issues.
volatile int * yourmother = blah(); if (yourmother) { // Nothing to see here }
Since Lumberyard considers this particular warning as a compiler error, we have to ignore it. I copied and pasted this from somewhere else in Lumberyard, so don’t blame me for it.
class UnitTestRaycast : public AllocatorsFixture
AllocatorsFixture
sets up certain internal allocation stuff that Lumberyard needs. I copied and pasted from somewhere else but check out Tests/TestTypes.h
if you want to see what it is doing. It’s not that complex.
Written by Greg Horvay
One more useful resource on Google Mock framework: its cookbook
https://github.com/google/googletest/blob/master/googlemock/docs/CookBook.md
LikeLike