Best practices
How to write tests that check what a user gets, and keep passing while the code changes.
A good test reads like what a user does, and fails only when what they get changes. These habits get there.
Test the app, not its parts
Render the application component (src/App.tsx, which main.tsx only
mounts) or a whole feature, and test it through what it shows. Tests of a
component's internals break on every refactor; tests of its behavior break
when the behavior does.
Find controls as a user does
Query by role and name first: screen.getByRole('button', { name: 'Save' }).
It is what a screen reader announces, so a test that finds a control this
way also checks that the control is accessible. Then
ByLabelText and
ByText. There are no test ids: a control a test
cannot find by its role and name is a control a screen-reader user cannot
find either.
// Good: what it is and what it is called.
screen.getByRole('adjustable', { name: 'Gain' });
// Brittle: the copy changes, and nothing says it is a button.
screen.getByText('Reset');Act with userEvent
userEvent does what a user does: a click is
pointer down, up and a click, with focus moving and the default actions
following, as in the runtime. fireEvent sends
one event and nothing else; keep it for what userEvent cannot do (a pen,
a touch, an accessibility action). Make one session per test:
const user = userEvent.setup().
Await everything
render, every interaction, waitFor, findBy… and advanceTime return
promises: await each. React's updates are applied when they settle; a
missing await shows as a test that passes or fails by chance.
Use the real contract
The plugin's parameters and native API come from its config. Set state with
testParameters and
testHost, mock native methods with
mockNative, in the test or in beforeEach.
Check a parameter through soundor:parameters itself:
expect(parameters.gain.get()).toBe(0.5).
Wait for what is asynchronous, and only that
Storage, files, native async methods and effects settle later: wait for
their result with waitFor or a findBy query.
Animations do not run on their own: move time with
advanceTime and check where they ended
(node.presentation). Never wait with setTimeout.
Keep tests apart
Every test gets a fresh runtime: nodes, parameters, storage and the host
start over. Put setup in beforeEach or the test itself, never in
beforeAll or at the top of the file, where it runs once against a runtime
that is gone by the first test.
Know what a test cannot check
There is no layout, no drawing and no audio in a test. Positions, pixels,
canvases and sound need the plugin: soundor dev, and
the native and browser tests of the runtimes themselves.