How do you write good unit tests?
JavaScriptThe short answer
Good unit tests are fast, isolated, readable, and test behavior not implementation. Follow the Arrange-Act-Assert pattern, test one thing per test, use descriptive test names, and mock external dependencies. Tests should give you confidence that your code works without being brittle to refactoring.
The Arrange-Act-Assert pattern
Every test should follow this structure:
test('calculates total with tax', () => { // Arrange — set up the data const items = [{ price: 10 }, { price: 20 }]; const taxRate = 0.1; // Act — run the function const total = calculateTotal(items, taxRate); // Assert — check the result expect(total).toBe(33);});This makes tests easy to read — you can immediately see what is being set up, what is being tested, and what the expected result is.
Test behavior, not implementation
// Bad — tests implementation detailstest('sets isLoading to true then false', () => { const { result } = renderHook(() => useUsers()); expect(result.current.isLoading).toBe(true); // ... wait ... expect(result.current.isLoading).toBe(false);});// Good — tests behaviortest('displays users after loading', async () => { render(<UserList />); expect( screen.getByText('Loading...') ).toBeInTheDocument(); expect( await screen.findByText('John') ).toBeInTheDocument();});The first test is coupled to the internal state. If you rename isLoading, the test breaks even though the component still works. The second test checks what the user sees — it survives refactoring.
Use descriptive test names
// Badtest('test1', () => { ... });test('works', () => { ... });// Goodtest('returns empty array when no items match the filter', () => { ... });test('throws an error when the input is not a number', () => { ... });A good test name tells you what broke without reading the test code.
One assertion per concept
Each test should verify one thing. If it fails, you immediately know what is wrong.
// Bad — too many things in one testtest('user module', () => { expect(createUser('John')).toBeDefined(); expect(getUser(1).name).toBe('John'); expect(deleteUser(1)).toBe(true); expect(getUser(1)).toBeNull();});// Good — separate teststest('creates a user', () => { expect(createUser('John')).toBeDefined();});test('retrieves a user by id', () => { const user = getUser(1); expect(user.name).toBe('John');});test('deletes a user', () => { expect(deleteUser(1)).toBe(true); expect(getUser(1)).toBeNull();});Test edge cases
Do not only test the happy path. Test boundaries and error cases:
test('returns 0 for empty array', () => { expect(sum([])).toBe(0);});test('handles negative numbers', () => { expect(sum([-1, -2, -3])).toBe(-6);});test('throws for non-array input', () => { expect(() => sum('hello')).toThrow();});Keep tests independent
Tests should not depend on each other. Each test should set up its own data and clean up after itself.
// Bad — tests share statelet user;test('creates user', () => { user = createUser('John');});test('updates user', () => { updateUser(user.id, { name: 'Jane' }); // depends on previous test});If the first test fails, the second test also fails even though the update logic might be fine.
Interview Tip
Show that you know the AAA pattern (Arrange-Act-Assert) and that you test behavior, not implementation. Give an example of a good test name and explain why testing edge cases matters. If the interviewer asks about React testing specifically, mention React Testing Library and testing what the user sees rather than internal state.
Why interviewers ask this
Writing good tests is a skill that separates experienced developers from beginners. Interviewers want to see if you can write tests that are maintainable, meaningful, and not brittle. A candidate who talks about testing behavior over implementation and keeping tests independent shows real-world testing experience.