Testing¶
Guide to writing and running tests for VulkanW3DViewer.
Overview¶
The project uses Google Test for unit testing. Tests are organized to mirror the src/ directory structure.
Running Tests¶
Build and Run¶
# Configure with tests enabled
cmake --preset test
# Build tests
cmake --build --preset test
# Run all tests
ctest --preset test
Parallel Execution¶
Speed up test runs with parallel execution:
Verbose Output¶
See detailed test output:
Run Specific Tests¶
Filter by name:
# Run tests matching pattern
ctest --preset test -R "ChunkReader"
# Run a single test
ctest --preset test -R "ChunkReaderTest.ReadUint32"
Test Structure¶
Directory Layout¶
tests/
├── CMakeLists.txt # Test configuration
├── w3d/ # W3D parser tests
│ ├── test_chunk_reader.cpp
│ ├── test_loader.cpp
│ ├── test_mesh_parser.cpp
│ ├── test_hierarchy_parser.cpp
│ ├── test_animation_parser.cpp
│ └── test_hlod_parser.cpp
├── render/ # Rendering tests
│ ├── test_animation_player.cpp
│ ├── test_bounding_box.cpp
│ ├── test_mesh_converter.cpp
│ ├── test_skeleton_pose.cpp
│ ├── test_texture_loading.cpp
│ └── raycast_test.cpp
└── stubs/ # Mock implementations
└── core/
└── pipeline.hpp
Naming Convention¶
- Test files:
test_<module>.cpp - Test fixtures:
<Module>Test - Test cases:
TEST_F(<Fixture>, <TestName>)
Writing Tests¶
Basic Test¶
#include <gtest/gtest.h>
#include "w3d/chunk_reader.hpp"
TEST(ChunkReaderTest, ReadUint32) {
std::vector<uint8_t> data = {0x01, 0x02, 0x03, 0x04};
w3d::ChunkReader reader(data);
uint32_t value = reader.read<uint32_t>();
EXPECT_EQ(value, 0x04030201); // Little-endian
}
Test Fixtures¶
Use fixtures for shared setup:
class MeshParserTest : public ::testing::Test {
protected:
void SetUp() override {
// Create test data
testMesh = createTestMesh();
}
void TearDown() override {
// Cleanup if needed
}
w3d::Mesh testMesh;
w3d::Mesh createTestMesh() {
w3d::Mesh mesh;
mesh.header.numVertices = 3;
mesh.vertices = {{0,0,0}, {1,0,0}, {0,1,0}};
return mesh;
}
};
TEST_F(MeshParserTest, ParsesVertices) {
EXPECT_EQ(testMesh.vertices.size(), 3);
}
TEST_F(MeshParserTest, ComputesBoundingBox) {
auto bounds = computeBounds(testMesh);
EXPECT_EQ(bounds.min, glm::vec3(0, 0, 0));
EXPECT_EQ(bounds.max, glm::vec3(1, 1, 0));
}
Parameterized Tests¶
Test multiple inputs:
class QuaternionConversionTest :
public ::testing::TestWithParam<std::tuple<w3d::Quaternion, glm::quat>> {
};
TEST_P(QuaternionConversionTest, ConvertsCorrectly) {
auto [w3dQuat, expected] = GetParam();
glm::quat result = convertQuaternion(w3dQuat);
EXPECT_NEAR(result.w, expected.w, 0.0001f);
EXPECT_NEAR(result.x, expected.x, 0.0001f);
EXPECT_NEAR(result.y, expected.y, 0.0001f);
EXPECT_NEAR(result.z, expected.z, 0.0001f);
}
INSTANTIATE_TEST_SUITE_P(
QuaternionTests,
QuaternionConversionTest,
::testing::Values(
std::make_tuple(w3d::Quaternion{0, 0, 0, 1}, glm::quat(1, 0, 0, 0)),
std::make_tuple(w3d::Quaternion{1, 0, 0, 0}, glm::quat(0, 1, 0, 0))
)
);
Assertions¶
Use appropriate assertions:
// Value comparisons
EXPECT_EQ(a, b); // a == b
EXPECT_NE(a, b); // a != b
EXPECT_LT(a, b); // a < b
EXPECT_LE(a, b); // a <= b
EXPECT_GT(a, b); // a > b
EXPECT_GE(a, b); // a >= b
// Floating point (with tolerance)
EXPECT_FLOAT_EQ(a, b); // Nearly equal (4 ULP)
EXPECT_NEAR(a, b, epsilon); // Within epsilon
// Boolean
EXPECT_TRUE(condition);
EXPECT_FALSE(condition);
// Strings
EXPECT_STREQ(a, b); // C strings equal
EXPECT_STRNE(a, b); // C strings not equal
// Exceptions
EXPECT_THROW(func(), std::runtime_error);
EXPECT_NO_THROW(func());
// Death tests (for crashes)
EXPECT_DEATH(func(), "error message");
Use ASSERT_* for fatal errors (stops test immediately):
ASSERT_NE(pointer, nullptr); // Must not be null
pointer->doSomething(); // Safe to use after assertion
Mock Objects¶
Stubs Directory¶
For testing without Vulkan, use stub implementations:
// tests/stubs/core/pipeline.hpp
namespace w3d {
class Pipeline {
public:
Pipeline() = default;
// Stub methods that do nothing
void bind(vk::CommandBuffer) {}
void pushConstants(const void*, size_t) {}
};
}
Using Stubs¶
#include "stubs/core/pipeline.hpp"
TEST(RenderTest, DrawsWithPipeline) {
w3d::Pipeline stubPipeline; // Uses stub
// Test drawing logic without actual Vulkan
}
Test Data¶
Creating Synthetic Data¶
std::vector<uint8_t> createMeshChunk() {
std::vector<uint8_t> data;
// Chunk header
appendU32(data, static_cast<uint32_t>(ChunkType::MESH));
appendU32(data, 100 | 0x80000000); // Container bit
// Sub-chunks...
return data;
}
Binary Helpers¶
void appendU32(std::vector<uint8_t>& data, uint32_t value) {
data.push_back(value & 0xFF);
data.push_back((value >> 8) & 0xFF);
data.push_back((value >> 16) & 0xFF);
data.push_back((value >> 24) & 0xFF);
}
void appendFloat(std::vector<uint8_t>& data, float value) {
uint32_t bits;
memcpy(&bits, &value, sizeof(float));
appendU32(data, bits);
}
Test-Driven Development¶
TDD Workflow¶
-
Write failing test
-
Run test (should fail)
-
Implement feature
-
Run test (should pass)
-
Refactor if needed
Example: Adding Animation Blending¶
// 1. Write test first
TEST(AnimationBlendingTest, BlendsPositions) {
Pivot poseA{.translation = {0, 0, 0}};
Pivot poseB{.translation = {2, 0, 0}};
Pivot result = blendPivots(poseA, poseB, 0.5f);
EXPECT_NEAR(result.translation.x, 1.0f, 0.001f);
}
// 2. Implement
Pivot blendPivots(const Pivot& a, const Pivot& b, float t) {
return Pivot{
.translation = glm::mix(a.translation, b.translation, t),
.rotation = glm::slerp(a.rotation, b.rotation, t)
};
}
Coverage¶
Generating Coverage¶
# Build with coverage flags
cmake --preset test -DCMAKE_CXX_FLAGS="--coverage"
cmake --build --preset test
# Run tests
ctest --preset test
# Generate report
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage-report
Viewing Coverage¶
Open coverage-report/index.html in a browser.
Continuous Integration¶
Tests run automatically on pull requests via GitHub Actions:
- All tests must pass
- Coverage should not decrease significantly
- New features should include tests