Creating a custom test suite (technical article)
Note: This article was written on 2025-01-05, as a summary of the events happening on Discord back during this development log
As part of my development of the navmesh system, I decided to add some tests to the existing test suite. Here's a technical article about the test suite system, how it's implemented, and the basics of creating a testing framework with custom code!
The original test suite concept was added to Project Mirabelle a while ago (back in September 2023, according to the commit history), mostly to provide a place to have "runnable tests". The idea of the original test suite was simply to call some methods inside of a runTests()
method.
export function runTests() { runBoxCollisionTests(); runIntersectionTests(); runNavmeshTests(); }
and then when opening the webpage with the ?testing
query parameter, the game wouldn't be started and the runTests()
method would be started instead.
const url = new URLSearchParams(window.location.search); if (url.has("testing")) { runTests(); } else { startGame(); }
Of course, having all of the tests inside of a method with each test defining their own content and logging the results in their own way isn't ideal. So I decided to implement a testing framework to standardize this.
The framework is using a simple define
/ it
/ expect
concept, with a custom test runner inspired by the mocha test framework. Writing a test looks like this:
define("Test suite", () => { it("Should pass a test", () => { const value = true; expectStrictlyEquals(true, value); }); });
The "custom test runner" was written as part of the src/utils/__tests__/test-utils.ts
file. Here is the idea, slightly shortened in terms of pretty logging for the example, but it's "really simple" as far as code goes (implemented in less than 30 lines) - the define()
method just adds a test suite to the list without running it, then there is a runTests()
method that runs all of the defined test suites. The it()
method is just a fancy try/catch to show information in the console, and the expectStrictlyEquals()
method just throws if the values don't match.
const testSuites = []; function define(name, callback) { testSuites.push({ name, callback }); } function it(name, callback) { console.log(` Running test ${name}`); try { callback(); console.log(` ✅ Test ${name} successful!`); } catch (e) { console.error(` ❌ Test ${name} failed: `, e); } } function expectStrictlyEquals(expected, current) { if (expected !== current) { throw new Error( `Expected ${expected}, got ${current}`, ); } } function runTests() { for (const suite of testSuites) { console.log(`Running test suite ${suite.name}`); suite.callback(); } }
And here is the test suite in action!