Unit Testing Matter.js Physics with Jest
This guide demonstrates how to write deterministic unit tests for custom physics behaviors using Jest and headless Matter.js. By running Matter.js directly inside Node.js without a canvas renderer or the browser DOM, you can isolate game mechanics, advance simulation frames manually, and verify properties such as positions, velocities, and collision responses using Jest assertions.
Setting Up the Headless Environment
Matter.js operates independently of the DOM when you bypass its
Render and Runner modules. To test custom
behaviors, install the required packages:
npm install matter-js
npm install --save-dev jestEnsure your jest.config.js is set to the Node test
environment:
module.exports = {
testEnvironment: 'node',
};Implementing Deterministic Stepping
Automated tests must be deterministic. Avoid real-time runners
(Matter.Runner) because execution speed variations cause
test flakiness. Instead, advance the physics engine manually using
Engine.update() with a fixed delta time (such as 16.666ms
for 60 FPS).
const advanceFrames = (engine, frames = 1, delta = 1000 / 60) => {
for (let i = 0; i < frames; i++) {
Engine.update(engine, delta);
}
};Testing a Custom Behavior: Example
Consider a custom behavior function that applies a continuous upward buoyancy force to objects submerged in a designated zone.
// buoyancy.js
const Matter = require('matter-js');
function applyBuoyancy(body, fluidDensity = 0.002) {
const forceMagnitude = body.mass * fluidDensity;
Matter.Body.applyForce(body, body.position, { x: 0, y: -forceMagnitude });
}
module.exports = { applyBuoyancy };To test this behavior, create a test suite that initializes the
Engine, adds a dynamic body, applies the behavior across
several frames, and asserts the resulting position or velocity:
// buoyancy.test.js
const Matter = require('matter-js');
const { applyBuoyancy } = require('./buoyancy');
const { Engine, World, Bodies } = Matter;
describe('Buoyancy Behavior', () => {
let engine;
let body;
beforeEach(() => {
// Create a new engine with zero gravity to isolate the custom force
engine = Engine.create({
gravity: { x: 0, y: 0, scale: 0 }
});
body = Bodies.rectangle(100, 100, 20, 20, { mass: 2 });
World.add(engine.world, body);
});
afterEach(() => {
World.clear(engine.world, false);
Engine.clear(engine);
});
test('should push the body upward when buoyancy is applied', () => {
const fixedDelta = 1000 / 60;
// Simulate 30 frames of the custom force
for (let i = 0; i < 30; i++) {
applyBuoyancy(body, 0.05);
Engine.update(engine, fixedDelta);
}
// Body should have moved upward (decreasing Y in Matter.js coordinates)
expect(body.velocity.y).toBeLessThan(0);
expect(body.position.y).toBeLessThan(100);
});
});Testing Collision Events Headless
Custom logic often relies on collision triggers. You can test
collision listeners directly through Matter.js's Events
module:
test('triggers custom damage logic on high-velocity collision', () => {
let damageApplied = false;
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach((pair) => {
const { bodyA, bodyB } = pair;
if (bodyA.label === 'player' || bodyB.label === 'player') {
damageApplied = true;
}
});
});
const player = Bodies.rectangle(100, 100, 20, 20, { label: 'player' });
const obstacle = Bodies.rectangle(100, 120, 20, 20, { isStatic: true });
World.add(engine.world, [player, obstacle]);
// Apply downward velocity directly to force collision
Matter.Body.setVelocity(player, { x: 0, y: 10 });
Engine.update(engine, 1000 / 60);
expect(damageApplied).toBe(true);
});Best Practices for Matter.js Physics Tests
- Use Approximate Matchers: Physics engines involve
continuous floating-point arithmetic. Use Jest’s
toBeCloseTo()instead oftoBe()when comparing body coordinates or velocities. - Isolate Default Gravity: Matter.js defaults to
standard downward gravity (
gravity.y = 1). Set gravity scales to0when testing non-gravitational behaviors to eliminate confounding forces. - Isolate Test State: Always clear the engine and
world instances between tests using
beforeEachandafterEachto prevent object state leakage.