Skip to content

Instantly share code, notes, and snippets.

@cblanquera
Last active August 29, 2025 02:45
Show Gist options
  • Select an option

  • Save cblanquera/5f4f6174e506e6e06ac9aad67f6bc8da to your computer and use it in GitHub Desktop.

Select an option

Save cblanquera/5f4f6174e506e6e06ac9aad67f6bc8da to your computer and use it in GitHub Desktop.
AI context so it can write better test suites using jest, typescript and reactjs.

Unit Testing Best Practices with Jest + TypeScript (and React)

This context file provides best practices for writing unit tests using Jest with TypeScript, and later, React.
It emphasizes clarity, determinism, and maintainability.


Quick-Start Checklist

  • Mirror source file paths for test files (src/x/y.tstests/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; prefer unknown and 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.

Jest + TypeScript Best Practices

Philosophy

  • Test public behavior, not private implementation.
  • Each test should fail for exactly one clear reason.

Organization

  • Use descriptive names:
    // ✅ Good
    it("returns null when user is not found", () => { ... });
    
    // ❌ Bad
    it("test getUser", () => { ... });

Type Safety

  • Always respect strict TS rules.
  • Use unknown if type is uncertain, never any.
// ✅ 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);
}

Test Data & Fixtures

  • Use builders/factories to avoid repetition:
// ✅ Good
const makeUser = (overrides: Partial<User> = {}): User => ({
  id: "u1",
  name: "Test User",
  email: "[email protected]",
  ...overrides,
});

Isolation & State

  • Use beforeEach / afterEach for clean setup/teardown.
  • Avoid leaking state across tests.
beforeEach(() => jest.resetModules());
afterEach(() => jest.clearAllMocks());

Mocking & Spies

  • Mock external boundaries only:
// ✅ Good
jest.spyOn(Math, "random").mockReturnValue(0.42);

// ❌ Bad (mocking internals)
jest.spyOn(myService, "helperFunction").mockReturnValue(...);

Asynchrony

  • Always await.
  • Prefer findBy queries or waitFor, not timeouts.
// ✅ Good
await expect(service.doWork()).resolves.toEqual("done");

// ❌ Bad
setTimeout(() => expect(result).toBe("done"), 1000);

Error Handling

  • Assert owned error messages or error shape.
  • Avoid brittle tests tied to third-party error wording.

React-Specific Best Practices

Testing Goals

  • 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);

Interactions

  • Use @testing-library/user-event for realistic events:
// ✅ Good
await userEvent.type(screen.getByRole("textbox"), "hello");
await userEvent.click(screen.getByRole("button", { name: /go/i }));

State & Effects

  • Test effects via observable behavior, not hook internals.

Bad Smells Table

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

CI & Coverage

  • Keep tests fast (<100ms each).
  • Ensure they pass in isolation and in any order.
  • Track mutation score (optional but useful).

Common Issues and Fixes

Async Mocks and mockResolvedValue Inference (never error)

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")); // ✅

Global Test APIs Not Typed

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"]
  }
}

jest-dom Matchers Missing

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"]
};

jest.spyOn Target Errors

Issue

jest.spyOn(api, 'getUser');
// ❌ No overload matches this call

Why
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' }); // ✅

Async Assertions Mis-Typed

Issue

expect(fetchUser()).resolves.toEqual({ id: 'u1' });
// ❌ This expression is not callable

Why
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' });

Parameterized Tests Typing (test.each)

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);
});

Path Aliases Mismatch

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"
  }
};

DOM/Window Globals Missing

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"]
  }
}

Custom Matchers / Extending Expect

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;
    }
  }
}

mockRejectedValueOnce() Override Bug

  • Issue: Calling .mockRejectedValueOnce() may not override an existing mockReturnValue.
  • Workaround: Use .mockImplementation() or .mockResolvedValue() instead.

React: Missing jsdom Environment

Issue

render(<App />);
// ❌ document is not defined

Why
Jest default environment is node.

Fix Set testEnvironment: "jsdom" in Jest config.

Example

// jest.config.js
module.exports = {
  testEnvironment: "jsdom"
};

React: user-event Without Await

Issue

userEvent.type(screen.getByRole('textbox'), 'hello');
expect(screen.getByDisplayValue('hello')).toBeInTheDocument();
// ❌ Assertion fails intermittently

Why
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();

React: Using getBy* Instead of findBy*

Issue

expect(screen.getByText(/loaded/i)).toBeInTheDocument();
// ❌ Throws immediately if element appears later

Why
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();

React: Custom Render Wrapper Typing

Issue

const renderWithProviders = (ui) =>
  render(<Provider>{ui}</Provider>);
// ❌ TypeScript complains about children

Why
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 });
};

React: act Warnings with Timers

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();
});

React: Router/Context Mocks

Issue

jest.mock('react-router-dom', () => ({
  useParams: jest.fn().mockReturnValue({ id: '123' }),
}));
// ❌ Type errors if useParams not mocked with correct type

Why
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' });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment