Last time I left you with a nagging little itch, and I did it on purpose. We gave your art a memory with Git - branches, tags, the whole time machine - and right at the end I said: a time machine lets you walk back from a break, but wouldn't it be better to catch the break the instant you make it? You tweak your noise function to fix one sketch and quietly wreck three others, and you don't notice for a week. Allez, that week-long gap between breaking something and finding out is exactly what we close today :-).
Now I know. The word "testing" makes half of you want to close the tab. It sounds like the least creative thing imaginable - unit tests, green checkmarks, some grey enterprise ritual that has nothing to do with making beautiful pictures. And honestly? If you're doodling a sketch for fun on a Sunday, it kind of doesn't. I'm not going to pretend every genuary doodle needs a test suite, because it doesn't and I'd be lying. But the moment your work has to last - a piece that runs unattended at an exhibition for eight hours, a tool other people rely on, a sketch you'll still be building on in a year - reliability stops being boring and starts being the thing that saves your whole weekend. Let me show you what I figured out.
Here's the idea the whole episode hangs on, and it's one we already built together way back. Remember episode 24, when we made randomness reproducible with a seed? Same seed in, same picture out, every single time. At the time that felt like a nice trick for saving compositions. But it's secretly the thing that makes creative code testable at all.
Think about why. You can't test something random - if the answer is different every run, there's nothing to check against. But a seeded sketch is a pure function in disguise: give it seed 42 and it will produce the exact same pixels today, tomorrow, and next year. And anything that gives a predictable answer is something you can pin down and guard.
// this is the whole foundation: seeded = deterministic = testable.
// the seeded rng from episode 24 - same seed, same sequence, forever.
function seeded(seed) {
let s = seed >>> 0;
return function () {
s = (s * 1664525 + 1013904223) >>> 0; // classic linear congruential step
return s / 4294967296; // normalise to 0..1
};
}
const a = seeded(42);
const b = seeded(42);
console.log(a() === b()); // true - two seed-42 streams march in lockstep
If those two streams didn't match, nothing below would work. So rule zero of testing creative code: make the thing deterministic first. No fixed seed, no test. That's why episode 24 wasn't just about saving art - it was quietly laying the foundation for this.
Before we test anything, we need the tiniest possible machinery to run tests. And here's a thing that took me too long to learn: you do not need Jest, you do not need a build step, you do not need to npm install anything. A test is just code that checks a thing is true and shouts if it isn't. Twelve lines and you're testing.
// a complete test runner. no dependencies, runs in any browser console or Node.
let passed = 0, failed = 0;
function it(name, fn) {
try {
fn(); // run the check; if it throws, it failed
passed++;
console.log(" ok " + name);
} catch (e) {
failed++;
console.error("FAIL " + name + " -> " + e.message);
}
}
function assert(cond, msg) {
if (!cond) throw new Error(msg || "assertion failed");
}
function report() { console.log(`\n${passed} passed, ${failed} failed`); }
That's it. That's a testing framework. it("thing works", () => assert(...)) and you're off. Everything the big frameworks add on top is convenience, not magic - and for creative work, this little thing carries you a shocking distance. I still reach for exactly this on small sketches.
The easiest and highest-value place to start is your little pile of maths utilities - the lerp, the map, the easing functions we've been collecting since the early episodes. These are pure functions: numbers in, numbers out, no canvas, no randomness. Which makes them stupidly easy to test, and stupidly important, because they're load-bearing - a wrong map() silently poisons every sketch that uses it.
// the helpers from episode 16, the ones EVERYTHING depends on.
function lerp(a, b, t) { return a + (b - a) * t; }
function map(v, a, b, c, d) { return c + (d - c) * ((v - a) / (b - a)); }
it("lerp hits both ends", () => {
assert(lerp(0, 10, 0) === 0, "t=0 should give the start");
assert(lerp(0, 10, 1) === 10, "t=1 should give the end");
});
it("lerp finds the middle", () => {
assert(lerp(0, 10, 0.5) === 5, "halfway should be 5");
});
it("map re-ranges correctly", () => {
assert(map(5, 0, 10, 0, 100) === 50, "5 in 0..10 is 50 in 0..100");
});
Run that and you get three green ok lines. Feels like nothing, right? But now try this: go "improve" your map function, fumble a minus sign, and instantly one of these goes red and tells you which case broke. That's the whole gift - not the passing tests, but the screaming when you break something. Makes sense, right? The test is a tripwire you set for your future clumsy self.
Okay, the maths helpers are the appetiser. Here's the main course, and it's the technique that genuinly changed how I ship creative work: snapshot testing, sometimes called golden-image testing. The idea is beautiful in its laziness. You render your sketch with a fixed seed, you take a "fingerprint" of the result, and you save that fingerprint as the known-good answer - the golden value. From then on, every time you run the test, you re-render and check the fingerprint still matches. If it changed, something changed, and you get to decide whether that was on purpose.
The fingerprint is just a hash of the pixels. Same pixels, same hash. One pixel different, wildly different hash.
// take a compact fingerprint of a canvas: hash every pixel into one number.
function snapshotHash(ctx, w, h) {
const data = ctx.getImageData(0, 0, w, h).data; // the raw RGBA bytes
let hash = 2166136261; // FNV-1a starting value
for (let i = 0; i < data.length; i++) {
hash ^= data[i];
hash = Math.imul(hash, 16777619); // FNV-1a mixing step
}
return hash >>> 0; // unsigned 32-bit fingerprint
}
Now the workflow. The first time, you render with a locked seed and print the hash - that printed number becomes your golden value, which you paste into the test (or, better, save to a file and commit it, exactly like the state files from last episode).
// render deterministically, then compare its fingerprint to the saved golden value.
const GOLDEN = 2831174902; // <- the hash you captured on a known-good run
it("flow field render is unchanged", () => {
const canvas = document.createElement("canvas");
canvas.width = 200; canvas.height = 200;
const ctx = canvas.getContext("2d");
renderFlowField(ctx, 42); // ALWAYS seed 42 - determinism is the point
const hash = snapshotHash(ctx, 200, 200);
assert(hash === GOLDEN, `render changed! got ${hash}, expected ${GOLDEN}`);
});
Sit with what this does for you. You refactor your flow-field code - clean it up, rename things, "improve" the noise - and you run the test. Green? Your refactor changed nothing about the actual picture, you're safe, ship it. Red? You changed the output, maybe on purpose, maybe not - and now you know, in the same minute you did it, not next week. This is the exact tripwire episode 136 promised. The whole week-long gap between breaking and noticing just collapsed to about four seconds.
And here's the lovely part when a change is intentional: you look at the new render, decide you like it better, and you simply update the golden value to the new hash. The test isn't a cage that freezes your art forever - it's a guard that makes sure changes are deliberate instead of accidental. Big difference.
Pure hashing has one honest weakness: it's brutally strict. One pixel one shade off and the hash is completely different. Sometimes that's exactly what you want, but sometimes it's too twitchy - a font renders a hair differently on another machine, an anti-aliased edge shifts by a fraction, and your test cries wolf even though the picture is visually identical. So the softer cousin of snapshot testing is a fuzzy pixel diff: instead of "are these byte-for-byte equal", you ask "how different are they, and is that under a threshold I'm comfortable with?"
// compare two renders pixel by pixel, return the FRACTION of pixels that differ.
function pixelDiff(a, b, tolerance) {
let differing = 0;
const n = a.length / 4; // number of pixels (RGBA = 4 bytes)
for (let i = 0; i < a.length; i += 4) {
const dr = Math.abs(a[i] - b[i]);
const dg = Math.abs(a[i+1] - b[i+1]);
const db = Math.abs(a[i+2] - b[i+2]);
if (dr + dg + db > tolerance) differing++; // this pixel counts as "changed"
}
return differing / n; // 0.0 = identical, 1.0 = totally different
}
Now your test says something much more human: "no more than half a percent of pixels are allowed to wander, and only by a little". That tolerates the tiny rendering wobbles you don't care about while still catching a real change - a shifted shape, a wrong colour, a missing layer.
it("render matches the golden image within tolerance", () => {
const now = renderToBytes(42); // fresh render, seed 42
const drift = pixelDiff(now, GOLDEN_BYTES, 12); // allow small per-pixel wobble
assert(drift < 0.005, `too much changed: ${(drift * 100).toFixed(2)}% of pixels`);
});
I reach for the strict hash on the pure-computation stuff (a fractal, a math pattern - those should be identical to the byte) and the fuzzy diff on anything involving text or heavy blending, where a little platform wobble is normal and fine. Pick the strictness that matches how twitchy the thing honestly is.
There's a third flavour of test that fits generative art almost too well, and I love it. Instead of checking an exact output, you check that certain rules always hold, no matter the seed. We call these invariants - things that must be true of every valid output, even though the outputs are all different. For art, the invariants are usually about staying in bounds: colours in range, counts non-negative, positions on the canvas.
// run the sketch across MANY seeds and assert a rule holds for all of them.
it("every particle stays a valid colour", () => {
for (let seed = 0; seed < 500; seed++) { // 500 different random worlds
const particles = generateParticles(seed);
for (const p of particles) {
assert(p.r >= 0 && p.r <= 255, `bad red ${p.r} at seed ${seed}`);
assert(p.alpha >= 0 && p.alpha <= 1, `bad alpha ${p.alpha} at seed ${seed}`);
}
}
});
This catches a whole class of bugs that a single snapshot never would. Maybe your colour code is fine for seed 42 but overflows for seed 199 because some rare branch multiplies two big numbers. A snapshot test on seed 42 sails straight past that. But throwing 500 seeds at an invariant finds it - it goes hunting through hundreds of little random universes looking for the one where your rule breaks. It's the difference between "does this one picture look right" and "is my system correct". For generative work, that second question is the one that actually matters.
Let me tell you about the single most common gremlin in creative code, because once you've been bitten you'll defend against it forever. It's NaN - "not a number" - and it is contagious. Divide by zero, take the square root of a negative, Math.acos something slightly out of range, and you get a NaN. And then the horror: any maths you do with a NaN produces another NaN. It spreads through your positions, your velocities, your colours, and suddenly your whole sketch is blank or frozen and you have no idea why, because the crash is silent - NaN doesn't throw an error, it just quietly poisons everything downstream.
The defence is a guard you can drop straight into your dev builds - an assertion that screams the instant a NaN appears, right where it's born, instead of ten frames later when the damage is everywhere.
// scream the MOMENT a NaN appears, at its birthplace - not ten frames downstream.
function assertFinite(value, label) {
if (!Number.isFinite(value)) {
throw new Error(`NaN/Infinity in ${label}: got ${value}`);
}
}
// sprinkle it through the hot path WHILE developing (strip or flag-off for release):
function updateParticle(p) {
p.vy += 0.1;
p.x += p.vx;
p.y += p.vy;
assertFinite(p.x, "particle.x"); // the tripwire fires at the exact source
assertFinite(p.y, "particle.y");
}
The first time this catches a NaN at its actual birthplace instead of letting you stare at a blank canvas for an hour, you'll want to buy it a drink. I've lost genuinely whole evenings to a single stray divide-by-zero before I started doing this. Now the guard just points at the exact line. Ten minutes of defence saves you those evenings forever.
Right, switch gears with me, because "tests pass" and "this thing will survive a gallery" are two very different promises. Back in episode 114 we talked about interactive installations - work that has to run unattended for hours, sometimes days, with nobody watching it. That's not about whether your maths is correct. It's about whether your sketch can survive time and the unexpected without falling over. And time is savage. A tiny memory leak that's invisible in a 30-second test becomes a crashed screen at hour six of an opening night, with a room full of people and you not there.
The first reliability move is: never let one bad frame kill the whole show. In a demo, an exception is fine - it stops, you see the red text, you fix it. In an installation, an unhandled exception is a black screen in front of the public. So you wrap the loop so a single ugly frame gets swallowed and logged instead of taking down the night.
// installation-grade loop: one bad frame must NEVER kill the whole show.
function safeLoop(drawFn) {
function frame(t) {
try {
drawFn(t); // your normal per-frame work
} catch (e) {
console.error("frame error (surviving it):", e); // log, don't die
// optionally: bump a counter, and if it's climbing fast, reset the scene
}
requestAnimationFrame(frame); // ALWAYS schedule the next frame regardless
}
requestAnimationFrame(frame);
}
That try around the draw call is the difference between a sketch that glitches for one frame and recovers, and one that dies in front of a crowd. For a Sunday doodle it's pure overkill. For a piece hanging in a space for a month, it's the seatbelt you're very glad you wore.
One frame surviving is good, but installations need more - they need to notice when they've drifted into a bad state and haul themselves back. This is a watchdog: a little timer that expects a regular "I'm still healthy" heartbeat, and if that heartbeat stops coming, it assumes the thing has wedged and resets it. Remember the object pools from episode 132? A classic slow-death bug is a pool that quietly keeps growing when it shouldn't - fine for a minute, fatal by hour four. A watchdog is how you catch the wedge without a human in the room.
// a watchdog: if the sketch stops reporting health, reset it automatically.
let lastBeat = performance.now();
let frameCount = 0;
function heartbeat() { lastBeat = performance.now(); frameCount++; } // call each good frame
setInterval(() => {
const silent = performance.now() - lastBeat;
if (silent > 3000) { // 3 seconds with no heartbeat = wedged
console.warn("watchdog: sketch went quiet, resetting scene");
resetScene(); // rebuild to a known-good clean state
lastBeat = performance.now();
}
}, 1000); // check once a second
And the health check can be positive too, not just "is it alive" - you can assert the sketch is behaving sanely. Is the particle pool still the size it should be? Has the frame time crept up and up (that's a leak breathing down your neck)? You measured frame time already back in episode 132; here you act on it automatically.
// a self-check the watchdog runs: is the system still in a sane shape?
function healthCheck(pool, frameMs) {
const problems = [];
if (pool.length > POOL_SIZE) problems.push(`pool grew to ${pool.length}`); // leak!
if (frameMs > 40) problems.push(`frame slow: ${frameMs.toFixed(1)}ms`); // stall
return problems; // empty array = all good
}
The mindset shift here is the real lesson: a demo just has to work once, while you watch. An installation has to keep working, alone, through things you didn't predict - a weird input, a slow memory creep, a browser hiccup at 3am. Testing proves it's correct today. Reliability engineering keeps it correct at hour two hundred. You need both, and they're genuinly different skills.
Let me be honest with you, because I'd hate for you to walk away thinking every sketch now needs a test suite and a watchdog. It absolutely does not, and over-testing art is its own trap - you can spend so long building scaffolding that you never make anything beautiful. So here's the actual judgement I use, and it's all about stakes:
A SUNDAY DOODLE: no tests. make the pretty thing. delete it or don't. be free.
A REUSABLE HELPER: test it. lerp, map, easing, colour maths - the load-bearing
stuff everything else stands on. bugs here poison everything.
A PIECE YOU'LL BUILD ON: snapshot-test the render. so a refactor next month can't
silently wreck the look you worked hard to find.
AN INSTALLATION / SHOW: the whole kit - snapshots, invariants, safeLoop, watchdog.
it has to survive hours alone in front of people.
See the pattern? The effort scales with what a failure costs. A broken doodle costs you nothing - shrug and remake it. A broken map() costs you every sketch that uses it. A crashed installation costs you an opening night and a chunk of your reputation. Spend your testing effort where the fall would actually hurt, and nowhere else. That's not laziness, that's aim.
Notice what's quietly happened over the last few episodes. We gave your work a memory (Git), we're now giving it a conscience (tests that catch your mistakes), and both of those are really about the same thing: making creative code you can trust and build on over time, not just throw away after one Sunday. That trust is the ground the last stretch of this series stands on. Because soon we stop making things only we can run, and start making things other people rely on - and the second someone else depends on your code, "it works on my machine" isn't good enough anymore. Tested, reliable code is the price of admission for building tools that outlive the afternoon you made them. So this week: take one helper file - your maths utilities - and write five real tests for it with the tiny runner from up top. Then snapshot-test one sketch you actually care about, break it on purpose, and watch the test go red. Feel that little jolt of "oh, it caught me". That jolt is the whole point, and once you've felt it you'll want it on everything that matters :-).
it/assert/report runner carries you a shocking distancelerp, map, easing). They're pure functions, trivial to test, and load-bearing - a wrong map() silently poisons every sketch that uses itassertFinite guard screams at the birthplace instead of leaving you staring at a blank canvasSo that's the answer to the itch I left you with last time. You don't have to wait a week to find out you broke something - you set a tripwire and it catches you in the same breath. And the quiet magic, as always, is how little it takes: a seed, a hash, twelve lines of runner, and the nerve to break your own work on purpose just to watch the alarm go off. Do that on one sketch you love this week, feel the little jolt when it goes red, and you'll never ship a silent break again. Merci for reading, and go set a trap for your future clumsy self :-).
Sallukes! Thanks for reading.
X