Project Mirabelle - Blog

Creating a custom test suite (technical article)

Written by ♫Dex, published 2024-06-22

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!

Screenshot of the test suite in action prior to adding the events system