Tutorial 99: Unit Testing CNA Game Logic

CNA — C++ XNA 4.0 reimplementation

CNA's own test suite (4,373 tests with ctest)

CNA ships 4,373 GoogleTest-based unit tests covering: math types (Vector2, Vector3, Matrix, Quaternion), geometry (BoundingBox, BoundingFrustum, Plane, Ray), curves (Bezier, Hermite), game loop semantics (fixed vs variable timestep), PackedVector precision, and color conversions. Run them with:

cmake --build build --target CnaTests
ctest --test-dir build --output-on-failure
# Expected: 4357 tests, 0 failures

Testing pure logic separately from rendering

Keep game logic in classes that do not depend on GraphicsDevice. Test those classes with GoogleTest independently. Only wire them to Game in the final game executable.

// GameLogic.hpp -- no CNA graphics dependency
class ScoreSystem {
public:
    void AddScore(int points, int multiplier = 1) {
        score_ += points * multiplier;
    }
    int Score() const { return score_; }
    void Reset()      { score_ = 0; }
private:
    int score_ = 0;
};

GoogleTest integration

# CMakeLists.txt for tests
cmake_minimum_required(VERSION 3.20)
project(MyGameTests CXX)
set(CMAKE_CXX_STANDARD 23)

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        v1.14.0
)
FetchContent_MakeAvailable(googletest)

# Game logic sources (no rendering)
add_library(GameLogic STATIC
    src/ScoreSystem.cpp
    src/PhysicsLogic.cpp
    src/AIController.cpp
)

# Test executable
add_executable(MyGameTests
    tests/ScoreSystemTest.cpp
    tests/PhysicsLogicTest.cpp
    tests/AIControllerTest.cpp
)
target_link_libraries(MyGameTests PRIVATE
    GameLogic
    GTest::gtest_main
)

include(GoogleTest)
gtest_discover_tests(MyGameTests)

Sample game logic test:

// tests/ScoreSystemTest.cpp
#include <gtest/gtest.h>
#include "ScoreSystem.hpp"

TEST(ScoreSystem, StartsAtZero) {
    ScoreSystem s;
    EXPECT_EQ(s.Score(), 0);
}

TEST(ScoreSystem, AddScore) {
    ScoreSystem s;
    s.AddScore(100);
    EXPECT_EQ(s.Score(), 100);
}

TEST(ScoreSystem, Multiplier) {
    ScoreSystem s;
    s.AddScore(50, 3);
    EXPECT_EQ(s.Score(), 150);
}

TEST(ScoreSystem, Reset) {
    ScoreSystem s;
    s.AddScore(999);
    s.Reset();
    EXPECT_EQ(s.Score(), 0);
}

// Test CNA math types directly (no GPU needed)
#include "Microsoft/Xna/Framework/Vector2.hpp"
using namespace Microsoft::Xna::Framework;

TEST(Vector2, Length) {
    Vector2 v(3.0f, 4.0f);
    EXPECT_NEAR(v.Length(), 5.0f, 1e-5f);
}

TEST(Vector2, Normalize) {
    Vector2 v(3.0f, 4.0f);
    v.Normalize();
    EXPECT_NEAR(v.Length(), 1.0f, 1e-5f);
}

TEST(Vector2, Lerp) {
    Vector2 a(0.0f, 0.0f), b(10.0f, 20.0f);
    Vector2 r = Vector2::Lerp(a, b, 0.5f);
    EXPECT_NEAR(r.X, 5.0f, 1e-5f);
    EXPECT_NEAR(r.Y, 10.0f, 1e-5f);
}

Headless CNA init for testing

Some tests need CNA initialized but no window. Set the environment variable SDL_VIDEODRIVER=offscreen before running tests to prevent SDL3 from opening a real window.

# Run tests headless (no display required)
SDL_VIDEODRIVER=offscreen ctest --test-dir build --output-on-failure
// In test fixture that needs GraphicsDevice:
class GraphicsTest : public ::testing::Test {
protected:
    void SetUp() override {
        // CNA headless: SDL_VIDEODRIVER=offscreen must be set
        // or skip GPU tests on CI
        if (!getenv("DISPLAY") && !getenv("WAYLAND_DISPLAY")) {
            GTEST_SKIP() << "No display available";
        }
    }
};

Mocking IDisposable resources

// Mock texture that tracks whether Dispose was called
class MockTexture2D {
public:
    bool disposed = false;
    int  width    = 64;
    int  height   = 64;
    void Dispose() { disposed = true; }
};

TEST(ContentCache, DisposesOnEvict) {
    ContentCache<MockTexture2D> cache(/*maxSize=*/2);
    auto* t1 = cache.Load("a");
    auto* t2 = cache.Load("b");
    auto* t3 = cache.Load("c");  // should evict t1
    EXPECT_TRUE(t1->disposed);
    EXPECT_FALSE(t2->disposed);
    EXPECT_FALSE(t3->disposed);
}

CI integration

# .github/workflows/tests.yml
name: CNA Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { submodules: recursive }
      - name: Install dependencies
        run: sudo apt-get install -y cmake g++-12 libgl1-mesa-dev
      - name: Configure
        run: cmake -S . -B build -DCNA_GRAPHICS_BACKEND=EASYGL -DCMAKE_BUILD_TYPE=Debug
      - name: Build
        run: cmake --build build --target CnaTests MyGameTests -j4
      - name: Run CNA tests
        run: ctest --test-dir build --output-on-failure
        env: { SDL_VIDEODRIVER: offscreen }
      - name: Run game tests
        run: ./build/MyGameTests