Tutorial 99: Unit Testing CNA Game Logic
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