
Why Testing Matters
Why Testing Is Important
We live in an era where we can write code without understanding it.
Ask an AI to build a function, it generates something that runs, and you move on.
That's the trap.
"It runs" and "it works" are not the same thing.
A function can execute cleanly and still be wrong. Wrong assumptions, wrong edge cases, wrong behavior under conditions you didn't think to check.
When you write every line yourself, you at least know what you don't know.
But when the code comes from somewhere else, you don't even have that.
Testing can close that gap by making a claim about what it should do, and checking if that claim holds.
It doesn’t matter if you wrote the function or an AI did.
A test just asks “does this behave the way it's supposed to?”
If you're a junior dev leaning on AI to code faster (and you should — that's the tool now), testing isn't the boring part you skip to hit a deadline.
It's the part that lets you trust code you didn't fully write.
Types of Testing
Not all tests check the same thing. Knowing the difference matters too.
Unit Test
Checks one piece in isolation. It could be a function, a class, a single unit of logic.
Fast, cheap, and the first thing you should reach for.
If a unit test fails, you know almost exactly where the bug is.
Integration Test
Checks if pieces work together.
E.g. your function talking to a database, an API, another module.
Your code might be perfect on its own and still malfunction combined with something else.
Functional Test
While an integration test checks if pieces talk to each other correctly, a functional test checks if outputs match the requirements.
Not just "can I query the database" but "did I get the specific value the feature requires."
End-to-End (E2E) Test
Replicates a whole user flow.
E.g. click the button, fill the form, see the result.
Closest to reality, but slow and expensive to maintain. Maybe a few key e2e tests, and lean on unit/integration tests to catch most breaks.
There are more categories too — acceptance tests, performance tests etc. — but the four above are what you'll write day to day.
Testing Frameworks
Once you know what to test, you need something to actually run the tests.
That's what a testing framework is for.
It gives you the structure (describe, it, expect) and the tools (assertions, mocking, running the suite) so you're not reinventing test infrastructure every time.
Here are some common JavaScript testing frameworks:
- Jest: The long-time default for JavaScript/React. Huge ecosystem, built-in mocking, works out of the box with most setups.
- Vitest: Built for Vite-based projects. Same API as Jest for the most part, but faster, and doesn't need extra config to understand your Vite setup.
- Mocha: Older, more flexible, but needs to be paired with a separate assertion library (like Chai).
They all do roughly the same job: let you write a claim (expect(x).toBe(y)), run it automatically, and tell you which ones failed.
Picking one usually comes down to your stack, not a strong opinion.
If you're on Vite, Vitest is the natural choice.
If you're on Create React App or an older setup, Jest is more likely already there.
Once your tests run from the terminal with a single command, you're one step away from automating them completely, which where CI/CD comes in, in the next post.
VORTEX Example
VORTEX is a browser-based DAW I’ve built using React, Vite, and Strudel.js (case study here).
One feature that I tested with Vitest is the loading of a project file (keyboard, guitar, bass, synth, drums, BPM saved as json).
Loading a file means trusting data you didn't generate yourself, which makes it a good example for the kind of testing this post is about.
Validating The File
Checking that a key exists isn't enough.
A key can be present and still be wrong.
E.g. data.drum might exist but be missing one of its instruments, or a note's gain field might be the string "loud" instead of a number.
The tests each check one specific way input can be malformed.
A missing track, a note missing a required field, a wrong field type, malformed settings:
it("rejects a note with the wrong field type", () => {
const data = validKeyboard();
(data.c.struct[0] as any).gain = "loud";
expect(isValidMelodicInstrument(data)).toBe(false);
});
it("rejects a file where a top-level key exists but is structurally corrupt", () => {
const data: any = validFileData();
data.drum = { settings: data.drum.settings };
expect(isValidSequencerFileData(data)).toBe(false);
});Each test asks one question.
When one fails, you know exactly which assumption broke.
Opening The File
Validating the shape is only half the job.
The app also needs to behave correctly when a file fails validation.
The open tests check that a bad file doesn't just get rejected, but leaves every store untouched:
it("leaves every store untouched when a nested field is structurally corrupt", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const before = snapshotState();
const data = validFileData();
(data.keyboard.c.struct[0] as any).gain = "loud";
await open(makeFile(JSON.stringify(data)));
expect(snapshotState()).toEqual(before);
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
});This is testing behavior, not implementation.
The test doesn't care what is wrong with the file.
It only cares that when it is wrong, nothing gets changed.

