React Testing Library Intro - discussion (2022-07-02)
If you're using React Testing Library, how do you keep tests stable when your app leans heavily on render signals and route-flow contracts?
Do you assert on user-visible output only, or do you treat signals as part of the test contract? What patterns help you avoid waiting/timing flake (especially with render fetch)? And how do you keep tests from turning into snapshots of implementation details?
Comments (10)
Back to latestWe assert on user-visible output first, then use signals to disambiguate.
If we assert only on signals, tests become too internal.
I like signals as a contract for why rather than what.
Example: assert the UI shows the right screen, then assert navIntent.reason to prove which guard/routine got you there.
We copied the pattern from Testing and Debugging and it made debugging failures dramatically faster.
We avoid timing waits by asserting on signal transitions instead of DOM timing.
ts
render(<App />);
await waitFor(() => expect(signal('fetch').status).toBe('ready'));
expect(screen.getByRole('heading', { name: /orders/i })).toBeInTheDocument();
The key is signals are set by the same contract that drives UI, so it stays deterministic across refactors.
Counterpoint: signals can make tests pass while the UX is broken (wrong text, focus, etc.).
We treat signals as secondary assertions only, never the primary proof.
Agree. The posture I like is: user-visible assertions + signals for explainability.
Signals are an observability layer, not the product.
Yep. The only time we lean on signals heavily is when we're testing navigation/guard contracts.
If your render fetch posture is bypass, you need deterministic test data. Otherwise you're just testing the network.
We stub at the fetch boundary and still render fetch signals so the app behaves the same way, just with predictable responses.
We keep a tiny helper that reads signals from the DOM (data attributes) so tests don't depend on internal module imports.
ts
export function signal(name: string) {
const el = document.querySelector(`[data-signal="${name}"]`);
return el ? JSON.parse(el.getAttribute('data-value') ?? 'null') : null;
}The trick is to keep signals stable and small. If signals are huge JSON blobs, your tests become snapshots anyway.
We used RTL queries for the main assertions and then used signals to triage failures.
It's a nice compromise: tests remain user-centric, but failures are explainable without rerunning locally with devtools.
If you're writing waitFor loops with arbitrary timeouts, add a signal instead.
You'll get a better app and better tests.
Related: Testing Signals (Deep Dive #12).
Also: don't make signals depend on localization. Keep them machine-readable, not stringly.