1. Test What Users See, Not What Developers Write

// 👎 Don't do this
test('component sets loading state', () => {
  const wrapper = shallow(<UserProfile />);
  expect(wrapper.state('isLoading')).toBe(true);
});

// 👍 Do this instead
test('shows loading state to users', () => {
  render(<UserProfile />);
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

2. Smart Ways to Test User Actions

// 👎 Testing implementation details
test('updates email state', () => {
  const wrapper = shallow(<SignupForm />);
  wrapper.find('input').simulate('change');
  expect(wrapper.state('email')).toBe('[email protected]');
});

// 👍 Testing user behavior
test('lets user submit their email', () => {
  const handleSubmit = jest.fn();
  render(<SignupForm onSubmit={handleSubmit} />);

  // Type like a real user would
  userEvent.type(
    screen.getByRole('textbox', { name: /email/i }), 
    '[email protected]'
  );

  // Click like a real user would
  userEvent.click(screen.getByRole('button', { name: /submit/i }));

  expect(handleSubmit).toHaveBeenCalledWith('[email protected]');
});

3. Stop Mocking Everything

// 👎 Over-mocking
jest.mock('./UserAvatar');
jest.mock('./UserBio');
jest.mock('./UserStats');

// 👍 Only mock what's necessary (like APIs)
const server = setupMSW([
  rest.get('/api/user', (req, res, ctx) => {
    return res(ctx.json({ name: 'John' }))
  })
]);

4. Testing Hooks? Keep It Simple

// 👎 Complex setup
const wrapper = mount(
  <Provider store={store}>
    <ThemeProvider theme={theme}>
      <HookComponent />
    ThemeProvider>
  Provider>
);

// 👍 Just test the logic
const { result } = renderHook(() => useCounter());
act(() => {
  result.current.increment();
});
expect(result.current.count).toBe(1);

5. Write Fewer, Better Tests (Meaningful tests)

// 👎 Testing every little thing
test('renders header');
test('renders subheader');
test('renders each list item');
test('renders footer');

// 👍 Test important flows
test('lets user complete checkout flow', async () => {
  render(<CheckoutFlow />);

  await userEvent.type(screen.getByLabelText(/card/i), '4242...');
  await userEvent.click(screen.getByText(/pay/i));

  expect(screen.getByText(/thanks for your order/i)).toBeInTheDocument();
});

6. Smart Selectors

// 👎 easy-to-break selectors
getByTestId('submit-button')
querySelector('.submit-btn')

// 👍 Resilient selectors
getByRole('button', { name: /submit/i })
getByLabelText(/email/i)
getByText(/welcome/i)

7. Testing Loading States

// 👍 Handle common async patterns
test('shows loading and then content', async () => {
  render(<UserProfile />);

  // Check loading state
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // Wait for data
  await screen.findByText('John Doe');

  // Loading should be gone
  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});

8. Test Error States

// 👍 Don't forget error handling
test('handles API errors gracefully', async () => {
  server.use(
    rest.get('/api/user', (req, res, ctx) => {
      return res(ctx.status(500))
    })
  );

  render(<UserProfile />);

  await screen.findByText(/something went wrong/i);
  expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument();
});

🎯 Quick Checklist Before Pushing

  • ✅ Tests pass
  • ✅ Tests are meaningful
  • ✅ Used realistic user interactions
  • ✅ Focused on behavior intead of implementation
  • ✅ Covered error states
  • ✅ Used resilient selectors
  • ✅ Mocked only what's necessary

💡 Remember

  • Tests should give you confidence to refactor
  • If tests break when you refactor (but the app works), your tests are wrong
  • Test behavior, not implementation
  • When in doubt, test like a user