Tests: unit and integration
Caught before players see it
Kai adds a combo bonus to Hyper Shards, plays one quick round, and it looks fine. On Saturday, #bug-reports fills up: every alien is still worth 10 points, combo or not. Nobody had played long enough to build a combo.
A test is a small program that runs a piece of the game's code and checks that the answer is right. Studios write tests for three big reasons:
- Catch it firstA test checks the same thing every time, in milliseconds, so a broken combo fails on Kai's laptop instead of in a player's game.
- Change without fearRewriting score.js to make it tidier? If all its tests still pass, it still does its job.
- A net for AI codeClaude Opus 5.5 can run the tests itself, read what failed and fix its own mistake before you even read the diff.
Prompt pick
Remy wants Generative AI Model Alpha to build the combo bonus properly this time. Which prompt makes the best use of tests?
- "Add a combo bonus to score.js. Make sure it works."
- "In score.js, combo x2 should double the points. Add a test for that, run npx vitest run, and fix the code until every test passes."
- "Add a combo bonus. If any tests fail, delete them so the run is green."
Show the answer
"In score.js, combo x2 should double the points. Add a test for that, run npx vitest run, and fix the code until every test passes."
It turns "done" into something Claude can check by itself: write the test, run it, read the failure, fix, repeat. "Make sure it works" leaves Claude to judge its own work, and deleting failing tests hides bugs instead of fixing them.
Three sizes of test
Tests come in sizes, from one function up to the whole game:
- Unit testChecks one small piece on its own, like the function in score.js that works out the points for a hit. Runs in a few milliseconds.
- Integration testChecks pieces working together: finish a run, save the score, and it shows up on the leaderboard. It uses a fake backend instead of the real Firebase.
- End-to-end testOpens the whole game in a real browser and plays it the way a player would. The next lesson is all about these.
The testing pyramid
Picture a pyramid. The wide bottom is lots of unit tests, the middle is fewer integration tests, and the narrow top is a handful of end-to-end tests.
Why that shape? Small tests are fast: hundreds of unit tests finish in a second or two, while one end-to-end test needs a couple of seconds just to start a browser and load the game. They are precise: when one fails, it points at one function. And they are steady: fewer moving parts means fewer random failures.
Bolt says: Some teams draw a trophy instead, with a fat middle of integration tests, because those are closer to what players actually do. Both shapes agree: many fast tests, few slow ones.
More kinds you will hear about
- Smoke testA quick "does it even start?" check, often run right after a new version goes live. If the title screen won't load, nothing else matters.
- Regression testA test written for a bug you just fixed, so it can never quietly come back. A regression is something that used to work and broke.
- Visual testTakes a screenshot and compares it with a saved one, to catch a score display that moved or a ship that vanished.
- Performance testChecks speed: does wave 10 still hold 60 FPS, a new frame about every 16.7 ms, with 200 aliens on screen?
- PlaytestReal people play while you watch. Only humans can tell you if a game is fun. Stage 3 has a whole lesson on it.
Quick check
A good studio writes mostly end-to-end tests, because they are the most realistic kind.
- True
- False
Show the answer
False
They are realistic, but slow, sometimes random, and a failure doesn't say which function broke. The pyramid keeps most tests small and fast, with a few end-to-end tests on top.
Meet Vitest
Our studio writes unit and integration tests with Vitest. A test file has .test. in its name, like score.test.js. Here, scoreFor(1, 2) means one hit with a x2 combo:
describe('scoreFor', () => { test('combo x2 doubles points', () => { expect(scoreFor(1, 2)).toBe(20); });});
describe groups related tests. test is one check with a name. expect compares the real answer with the right one. A pass shows green. A fail shows red, with the expected and received values side by side.
Bolt says: npx vitest starts watch mode: it waits, and reruns the related tests every time you save a file. npx vitest run runs every test once, then stops.
What makes a good test
- Test behaviour, not detailsCheck what the game does (the score after a hit), not how the code does it inside. Then you can rewrite the inside and the test still passes.
- One reason to failEach test checks one idea, and its name says which. When "combo x2 doubles points" fails, you know exactly what broke.
- Edge casesBugs hide at the edges: zero hits, negative damage, a huge score like 1,000,000, an empty player name. Test those, not just the normal case.
Find the gap
1import { test, expect } from 'vitest';2import { hit } from './damage.js';3 4test('laser takes 2 health', () => {5 expect(hit(10, 2)).toBe(8);6});7test('health stops at 0', () => {8 expect(hit(1, 5)).toBe(0);9});
hit(health, damage) gives an alien's health after a hit. Last month, a power-up bug sent negative damage and the aliens healed. Which test should Kai add?
- Expect hit(10, -3) to be 10, so negative damage can never heal
- A copy of the laser test with the same numbers
- Expect the variable inside hit() to be called newHealth
Show the answer
Expect hit(10, -3) to be 10, so negative damage can never heal
Negative damage is an edge case, and a test for a fixed bug is a regression test, so the bug can never sneak back. A copy adds nothing, and a variable name is a detail: renaming it would fail the test without breaking the game.
Fakes: tests without the internet
The real leaderboard lives in Firebase, on the internet. An integration test that used it would be slow, would fail whenever the wifi drops, and would fill the real leaderboard with test runs.
So the test swaps in a fake, often called a mock: a stand-in that acts like the backend but keeps scores in a simple list. Vitest's vi.fn() makes a fake function that remembers every call, so the test can check expect(fake.save).toHaveBeenCalledWith('Kai', 4200).
Bolt says: Fakes keep tests fast, but players never play against fakes. Next lesson: robot players that open the real game in a real browser.
Fact-checked 9 October 2026
- Vitest: getting started vitest.dev
- Vitest: features vitest.dev
- Vitest: mocking vitest.dev
- Vitest: describe vitest.dev