C++/CLI to the Rescue
Back in 2011, I needed to find a way to unit test my C++ code.
NUnit and Microsoft's C++/CLI were perfect for the task.
You can use assert wrappers, or directly inherit from the C++ class and test it that way - useful for exposing protected methods as well.
Here's the project being tested via NUnit:
One of the issues with this approach is the marshaling of strings. To address that, and other issues, I created a base class which provided useful functions and wrappers around NUnit's StringAssert:
static std::string AsString(String ^s){
std::string os;
MarshalString(s,os);
return os;
}
static std::wstring AsWString(String ^s){
std::wstring os;
MarshalString(s,os);
return os;
}
static void MarshalString(String^ s, std::string& os){
using namespace Runtime::InteropServices;
const char* chars=(const char*)(Marshal::StringToHGlobalAnsi(s)).ToPointer();
os=chars;
Marshal::FreeHGlobal(IntPtr((void*)chars));
}
static void MarshalString(String^ s, std::wstring& os){
using namespace Runtime::InteropServices;
const wchar_t* chars=(const wchar_t*)(Marshal::StringToHGlobalUni(s)).ToPointer();
os=chars;
Marshal::FreeHGlobal(IntPtr((void*)chars));
}
static void StringAssertContains(String^ expected, const std::string & actual){
String^ a=gcnew String(actual.c_str());
StringAssert::Contains(expected,a);
}
// etc….
The codebase had no indirection for injecting state, so I chose to attach/detach a test database between tests to guarantee a known baseline. That way, it's just a configuration setting change, no source code modifications required to support testing needs.
This worked but was I/O heavy. A better approach would have been to keep the database attached and wrap each test in a transaction (BEGIN TRAN), rolling back in teardown (ROLLBACK) to restore a clean state. Today I'd default to transaction rollback (or a truncate/reseed reset) for speed, reserving attach/detach for rare full-isolation cases.
Caveat: Transaction rollback works cleanly when the database operations under test participate in the same transaction. If the code opens independent connections, those connections may need to enlist in a shared transaction; otherwise a different reset strategy may be required.
Here's the attach/detach code — an interesting relic:
REM AttachDatabase.cmd
copy /Y c:\db\TestBase.mdf c:\db\Test.mdf
sqlcmd -S (LOCAL_SQL_SERVER)\SqlExpress -U sa -P (password) -i c:\db\AttachDatabase.sql
REM AttachDatabase.sql
USE [master]
GO
CREATE DATABASE [TestDatabase] ON
( FILENAME = N'C:\db\Test.mdf' ) FOR ATTACH ;
GO
REM DetachDatabase.cmd
sqlcmd -S (LOCAL_SQL_SERVER)\SqlExpress -U sa -P (password) -i c:\db\DetachDatabase.sql
del c:\db\Test.mdf
REM DetachDatabase.sql
USE [master]
GO
DROP DATABASE [TestDatabase]
GO
And next, a base test fixture to execute these commands:
[SetUp]
void TestSetup(void){
InitializeDatabase();
}
[TearDown]
void TestTeardown(void){
FinalizeDatabase();
}
void InitializeDatabase(void){
system(AsString(CreateDatabaseScript).c_str());
Domain.Initialize();
}
void FinalizeDatabase(void){
Domain.Finalize();
system(AsString(DeleteDatabaseScript).c_str());
}
Finally with the infrastructure in place, we can test our fixtures:
[TestFixture]
public ref class AvatarServiceFixture:BaseFixture{
public:
[Test]
void LoadAvatarTest(void){
CallResult<Avatar> avatarResult=AvatarService::GetAvatar(PLAYER1_ID);
Assert::IsTrue(avatarResult.IsSuccess());
Avatar & avatar=avatarResult.Value;
vector<AvatarObject> avatarObjects=AvatarObject::GetByAvatarId(avatar.GetId());
AvatarObjectFilter aoFilter(avatarObjects);
AvatarObjectFilter activeObjects=aoFilter.WhereIsActive(true);
AssertTransientActive(activeObjects,avatar.GetTop());
AssertTransientActive(activeObjects,avatar.GetAccessory());
// etc...
}
}