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:

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...
    }
}