Jest is the de facto testing framework for JavaScript and TypeScript projects in India, used by frontend and full-stack engineers at React and Node.js shops across the country. If you are interviewing for a frontend or full-stack role at an Indian product company, you will likely be asked to write or debug a Jest test on the spot.
Unit Testing Philosophy and Jest Test Structure
Understanding the philosophy behind unit testing is as important as knowing the Jest API. Interviewers at Indian product companies want to know that you write tests purposefully, not just to hit a coverage number.
Unit testing philosophy:
- Test the smallest unit in isolation: a pure function, a class method, a React component — tested without depending on real databases, APIs, or other services
- Tests should be FIRST: Fast (milliseconds per test), Independent (no shared state between tests), Repeatable (same result every run), Self-validating (pass or fail clearly), Timely (written alongside the code)
- Arrange, Act, Assert (AAA) pattern: set up the test data (Arrange), call the function under test (Act), verify the result (Assert)
- Test behaviour, not implementation: test what a function does, not how it does it internally; tests that depend on implementation details break when you refactor
Jest test structure:
- describe('groupName', () => { ... }) — groups related tests; can be nested
- it('should do X when Y', () => { ... }) or test('...', () => { ... }) — defines a single test case
- expect(value).toBe(expected) — strict equality (Object.is); use for primitives
- expect(value).toEqual(expected) — deep equality; use for objects and arrays
- expect(fn).toThrow('error message') — assert that a function throws
- expect(mock).toHaveBeenCalledWith(arg1, arg2) — assert a mock was called with specific arguments
- expect(mock).toHaveBeenCalledTimes(2) — assert a mock was called exactly twice
- expect(value).toBeTruthy() / .toBeFalsy() — loose truthiness checks
- expect(array).toContain(item) — assert an array contains a value
- expect(string).toMatch(/regex/) — assert a string matches a regex
Test lifecycle hooks:
- beforeAll(() => { ... }) — runs once before all tests in the describe block
- afterAll(() => { ... }) — runs once after all tests; use for cleanup
- beforeEach(() => { ... }) — runs before each test; use to reset state
- afterEach(() => { ... }) — runs after each test; use to clean up mocks (jest.clearAllMocks())
Mocking with jest.fn, jest.spyOn, and jest.mock
Mocking is the most important Jest skill tested in interviews. You need to isolate the unit under test from its dependencies.
jest.fn():
- Creates a mock function that records all calls: const mockFn = jest.fn()
- Returns undefined by default; configure a return value: mockFn.mockReturnValue(42)
- mockFn.mockReturnValueOnce(value) — return a value only on the next call; subsequent calls revert to default
- mockFn.mockImplementation(fn) — provide a full implementation: mockFn.mockImplementation((x) => x * 2)
- mockFn.mock.calls — array of all calls: [[arg1, arg2], [arg1, arg2], ...]
- mockFn.mock.results — array of return values: [{ type: 'return', value: 42 }, ...]
- jest.clearAllMocks() — clear mock.calls and mock.results but keep the implementation
- jest.resetAllMocks() — clear calls and reset implementation to undefined
jest.spyOn():
- Spies on an existing method without fully replacing it: jest.spyOn(object, 'methodName')
- The original method still runs unless you also call .mockImplementation() or .mockReturnValue()
- Use spyOn to assert that a method was called without changing its behaviour: const spy = jest.spyOn(console, 'error').mockImplementation(() => {})
- Restore the original after the test: spy.mockRestore()
jest.mock():
- Auto-mocks an entire module: jest.mock('./userService') — all exported functions become jest.fn()
- Manual mock: create a _mocks_ folder next to the module with a file of the same name
- jest.mock('../api', () => ({ fetchUser: jest.fn() })) — provide a factory for more control
- ES module mocking: jest.mock is hoisted to the top of the file by Babel; you must use jest.mock before imports that depend on it (or use jest.isolateModules)
Async testing:
- Return a Promise: test('should fetch', () => { return fetchData().then(data => expect(data).toBeDefined()) })
- async/await: test('should fetch', async () => { const data = await fetchData(); expect(data).toBeDefined(); })
- Mocking async functions: mockFn.mockResolvedValue(data) and mockFn.mockRejectedValue(new Error('fail'))
- Testing timers: jest.useFakeTimers() replaces setTimeout/setInterval with controllable mocks; call jest.runAllTimers() or jest.advanceTimersByTime(1000) to trigger them
Snapshot Testing, Coverage Reports, and React Testing Library
Snapshot testing and code coverage are commonly discussed in Jest interviews. For frontend roles, React Testing Library integration is also expected.
Snapshot testing:
- expect(component).toMatchSnapshot() — serialises the component/object and compares it to a stored snapshot file
- On the first run, Jest creates the snapshot file; on subsequent runs it compares against the stored snapshot
- If the output changes intentionally, update snapshots with: jest --updateSnapshot (or jest -u)
- Snapshot files (*.snap) should be committed to version control
- Use sparingly: snapshot tests catch accidental changes but can produce noisy diffs for large components; prefer explicit assertions for logic-critical behaviour
- toMatchInlineSnapshot() — stores the snapshot inline in the test file; useful for small values
Code coverage:
- jest --coverage generates a coverage report using Istanbul (built into Jest)
- Four coverage metrics: Statements (each statement executed), Branches (each branch of if/else/ternary/switch executed), Functions (each function called), Lines (each line executed)
- Configure in jest.config.js: coverageThreshold sets minimum coverage percentages; CI fails if below the threshold
- coveragePathIgnorePatterns: exclude generated files, vendor code, and type definitions
- Interpreting coverage: 100% coverage does not mean bug-free; test quality (assertions) matters more than coverage percentage; use coverage to find untested code paths, not as a goal in itself
React Testing Library:
- @testing-library/react renders components in a simulated DOM (jsdom) and provides queries
- Queries: getByText, getByRole, getByLabelText, getByTestId (use data-testid attributes); prefer getByRole (most accessible) and getByLabelText
- getBy throws if not found; queryBy returns null; findBy returns a Promise (for async elements)
- fireEvent.click(button) — trigger DOM events
- userEvent.type(input, 'text') — @testing-library/user-event simulates real user interactions more accurately than fireEvent
- waitFor(() => expect(...)) — retry the assertion until it passes or times out; use for async state updates
- act(): React Testing Library wraps renders and events in act() automatically; you only need it manually for custom hooks or asynchronous state updates outside of the library's utilities
Frequently asked questions
Explore more