This context file provides best practices for writing unit tests using Jest with TypeScript, and later, React.
It emphasizes clarity, determinism, and maintainability.
- Mirror source file paths for test files (
src/x/y.ts→tests/x/y.test.ts). - Write tests for behavior, not implementation.
- Keep tests independent and deterministic.
- Use factories/builders for test data.
- Mock only true boundaries (I/O, time, randomness, network).
- Reset state after each test.
- Use strict TypeScript (never
any; preferunknownand narrow). - Prefer explicit assertions over snapshots.
- For VSCode: Don't ask to save the file or run tests while VSCode is reporting type errors. Fix the type errors first.
- Remove unused variables
- Run tests in parallel; ensure they don’t rely on order.
- Test public behavior, not private implementation.
- Each test should fail for exactly one clear reason.
- Use descriptive names:
// ✅ Good it("returns null when user is not found", () => { ... }); // ❌ Bad it("test getUser", () => { ... });
- Always respect strict TS rules.
- Use
unknownif type is uncertain, neverany.
// ✅ Good
function parse(input: unknown): number | null {
if (typeof input === "string") return parseInt(input);
return null;
}
// ❌ Bad
function parse(input: any): number | null {
return parseInt(input);
}- Use builders/factories to avoid repetition:
// ✅ Good
const makeUser = (overrides: Partial<User> = {}): User => ({
id: "u1",
name: "Test User",
email: "[email protected]",
...overrides,
});- Use
beforeEach/afterEachfor clean setup/teardown. - Avoid leaking state across tests.
beforeEach(() => jest.resetModules());
afterEach(() => jest.clearAllMocks());- Mock external boundaries only:
// ✅ Good
jest.spyOn(Math, "random").mockReturnValue(0.42);
// ❌ Bad (mocking internals)
jest.spyOn(myService, "helperFunction").mockReturnValue(...);- Always
await. - Prefer
findByqueries orwaitFor, not timeouts.
// ✅ Good
await expect(service.doWork()).resolves.toEqual("done");
// ❌ Bad
setTimeout(() => expect(result).toBe("done"), 1000);- Assert owned error messages or error shape.
- Avoid brittle tests tied to third-party error wording.
- Test components like a user: observable DOM and interactions.
- Use React Testing Library (RTL), not shallow rendering.
// ✅ Good
render(<LoginForm />);
expect(screen.getByRole("button", { name: /submit/i })).toBeDisabled();
// ❌ Bad
const wrapper = shallow(<LoginForm />);
expect(wrapper.find("button").prop("disabled")).toBe(true);- Use
@testing-library/user-eventfor realistic events:
// ✅ Good
await userEvent.type(screen.getByRole("textbox"), "hello");
await userEvent.click(screen.getByRole("button", { name: /go/i }));- Test effects via observable behavior, not hook internals.
| Smell | Why It’s Bad | Better Practice |
|---|---|---|
Using any |
Removes type safety | Use unknown + narrowing |
| Over-mocking | Mirrors implementation | Mock only boundaries |
Brittle selectors (.class > div) |
Breaks on refactor | Use role/label queries |
| Unseeded randomness | Non-deterministic | Mock randomness |
| Large snapshots | Hard to review | Assert explicit expectations |
| Shared mutable fixtures | Coupled tests | Use local builders |
- Keep tests fast (<100ms each).
- Ensure they pass in isolation and in any order.
- Track mutation score (optional but useful).
Issue
const fetchUser = jest.fn();
fetchUser.mockResolvedValue({ id: 'u1' });
// ❌ Argument of type '{ id: string }' is not assignable to parameter of type 'never'.Why
Without explicit typing, jest.fn() infers () => any. mockResolvedValue expects the awaited type of a Promise, which falls back to never.
Fix
Provide an explicit Promise return type using helpers or jest.MockedFunction.
Example
// Helper
function mockAsync<T, A extends any[] = []>() {
return jest.fn<Promise<T>, A>();
}
const fetchUser = mockAsync<{ id: string }>();
fetchUser.mockResolvedValue({ id: 'u1' }); // ✅
const failingOp = mockAsync<void>();
failingOp.mockRejectedValue(new Error("boom")); // ✅Issue
Cannot find name 'describe'
Cannot find name 'expect'
Why
TypeScript doesn’t automatically include Jest’s global declarations.
Fix
Add Jest to types in your tsconfig for tests.
Example
{
"compilerOptions": {
"types": ["jest", "node"]
}
}Issue
expect(element).toBeInTheDocument();
// ❌ Property 'toBeInTheDocument' does not exist on type 'Matchers<...>'Why
@testing-library/jest-dom extends Jest’s matchers, but TypeScript only sees them if imported in setup.
Fix
Import @testing-library/jest-dom in a setup file, and include it in Jest config.
Example
// setupTests.ts
import '@testing-library/jest-dom';
// jest.config.js
module.exports = {
setupFilesAfterEnv: ["<rootDir>/tests/setupTests.ts"]
};Issue
jest.spyOn(api, 'getUser');
// ❌ No overload matches this callWhy
Spying on the wrong object (e.g., default export vs named export). TypeScript doesn’t see the function on the target object.
Fix Spy on the module namespace that owns the function.
Example
import * as api from './api';
jest.spyOn(api, 'getUser').mockResolvedValue({ id: 'u1' }); // ✅Issue
expect(fetchUser()).resolves.toEqual({ id: 'u1' });
// ❌ This expression is not callableWhy
Not awaiting a Promise; mixing sync matcher types with async calls.
Fix
Use await expect(...).resolves or await the promise before asserting.
Example
await expect(fetchUser()).resolves.toEqual({ id: 'u1' });
const result = await fetchUser();
expect(result).toEqual({ id: 'u1' });Issue
test.each([
['42', 42],
[42, 'oops'], // ❌ wrong types sneak in
])((input, expected) => { ... });Why
By default, Jest infers rows as any[].
Fix Type the row tuples.
Example
type Row = [input: string, expected: number];
test.each<Row>([
['42', 42],
['07', 7],
])('parses %s', (input, expected) => {
expect(parseInt(input)).toBe(expected);
});Issue
Cannot find module '@/utils/foo'
Why
TypeScript paths are not automatically mirrored in Jest.
Fix
Add equivalent mapping in Jest’s moduleNameMapper.
Example
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
// jest.config.js
module.exports = {
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1"
}
};Issue
window.matchMedia('(prefers-color-scheme: dark)');
// ❌ Property 'matchMedia' does not exist on type 'Window & typeof globalThis'Why
JSDOM doesn’t provide all browser APIs, or lib option in tsconfig is missing DOM types.
Fix Add a shim for missing APIs, and include DOM lib in tsconfig.
Example
Object.defineProperty(window, 'matchMedia', {
value: jest.fn().mockImplementation(() => ({
matches: false,
addListener: jest.fn(),
removeListener: jest.fn(),
})),
});
// tsconfig.json
{
"compilerOptions": {
"lib": ["esnext", "dom"]
}
}Issue
expect(user).toHaveFoo();
// ❌ Property 'toHaveFoo' does not exist on type 'Matchers<any>'Why
Custom matchers need global type augmentation.
Fix
Declare the matcher type in a .d.ts file.
Example
// jest.d.ts
declare global {
namespace jest {
interface Matchers<R> {
toHaveFoo(): R;
}
}
}- Issue: Calling
.mockRejectedValueOnce()may not override an existingmockReturnValue. - Workaround: Use
.mockImplementation()or.mockResolvedValue()instead.
Issue
render(<App />);
// ❌ document is not definedWhy
Jest default environment is node.
Fix
Set testEnvironment: "jsdom" in Jest config.
Example
// jest.config.js
module.exports = {
testEnvironment: "jsdom"
};Issue
userEvent.type(screen.getByRole('textbox'), 'hello');
expect(screen.getByDisplayValue('hello')).toBeInTheDocument();
// ❌ Assertion fails intermittentlyWhy
user-event is async; without await, UI state may not be updated.
Fix
Always await user-event interactions.
Example
await userEvent.type(screen.getByRole('textbox'), 'hello');
expect(screen.getByDisplayValue('hello')).toBeInTheDocument();Issue
expect(screen.getByText(/loaded/i)).toBeInTheDocument();
// ❌ Throws immediately if element appears laterWhy
getBy* is synchronous. For async UI, prefer findBy*.
Fix
Use await screen.findBy* when waiting for elements.
Example
expect(await screen.findByText(/loaded/i)).toBeInTheDocument();Issue
const renderWithProviders = (ui) =>
render(<Provider>{ui}</Provider>);
// ❌ TypeScript complains about childrenWhy
Wrapper component not typed with PropsWithChildren.
Fix Type wrapper correctly, and preserve RTL’s return type.
Example
const renderWithProviders = (ui: React.ReactElement) => {
const Wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
<Provider>{children}</Provider>
);
return render(ui, { wrapper: Wrapper });
};Issue
Warning: An update to Component inside a test was not wrapped in act(...)
Why
Fake timers advanced without React being flushed.
Fix
Wrap timer advances in act, or prefer user-facing waits.
Example
act(() => {
jest.runOnlyPendingTimers();
});Issue
jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockReturnValue({ id: '123' }),
}));
// ❌ Type errors if useParams not mocked with correct typeWhy
Mock shape doesn’t match hook type.
Fix
Use jest.MockedFunction to align with original hook.
Example
import { useParams } from 'react-router-dom';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn(),
}));
const mockedUseParams = useParams as jest.MockedFunction<typeof useParams>;
mockedUseParams.mockReturnValue({ id: '123' });