How do you write good unit tests?

JavaScript

The 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 details
test('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 behavior
test('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

// Bad
test('test1', () => { ... });
test('works', () => { ... });
// Good
test('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 test
test('user module', () => {
expect(createUser('John')).toBeDefined();
expect(getUser(1).name).toBe('John');
expect(deleteUser(1)).toBe(true);
expect(getUser(1)).toBeNull();
});
// Good — separate tests
test('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 state
let 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.