Frontend Testing (React)
Guia de testes para frontend React com Jest e React Testing Library.
Setup
Dependências
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom msw
package.json:
Configuração Jest
jest.config.js:
export default {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'^@/(.*)$': '<rootDir>/src/$1'
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.tsx',
'!src/main.tsx'
],
coverageThresholds: {
global: {
branches: 60,
functions: 60,
lines: 60,
statements: 60
}
}
};
src/setupTests.ts:
import '@testing-library/jest-dom';
import { server } from './mocks/server';
// Establish API mocking before all tests
beforeAll(() => server.listen());
// Reset handlers after each test
afterEach(() => server.resetHandlers());
// Clean up after all tests
afterAll(() => server.close());
Testing Components
Basic Component Test
// src/components/UserProfile/UserProfile.test.tsx
import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
it('renders user name', () => {
// Arrange
const user = {
id: 1,
name: 'John Doe',
email: 'john@example.com'
};
// Act
render(<UserProfile user={user} />);
// Assert
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
it('shows loading state', () => {
render(<UserProfile user={null} loading={true} />);
expect(screen.getByTestId('loader')).toBeInTheDocument();
});
it('shows error message when error occurs', () => {
render(<UserProfile user={null} error="Failed to load" />);
expect(screen.getByText(/failed to load/i)).toBeInTheDocument();
});
});
Testing User Interactions
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('submits form with email and password', async () => {
// Arrange
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
// Act
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /login/i }));
// Assert
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
it('shows validation error for invalid email', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={jest.fn()} />);
await user.type(screen.getByLabelText(/email/i), 'invalid-email');
await user.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
});
});
Testing Hooks
// src/hooks/useUser.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { useUser } from './useUser';
import { server } from '../mocks/server';
import { rest } from 'msw';
describe('useUser', () => {
it('fetches user data on mount', async () => {
// Arrange
const userId = 123;
// Act
const { result } = renderHook(() => useUser(userId));
// Assert - initial state
expect(result.current.loading).toBe(true);
expect(result.current.user).toBeNull();
// Assert - after fetch
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.user).toEqual({
id: 123,
name: 'John Doe',
email: 'john@example.com'
});
});
it('handles error when fetch fails', async () => {
// Arrange - Mock error response
server.use(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Server error' }));
})
);
// Act
const { result } = renderHook(() => useUser(123));
// Assert
await waitFor(() => {
expect(result.current.error).toBeTruthy();
});
});
});
Mocking APIs (MSW)
Setup MSW
src/mocks/handlers.ts:
import { rest } from 'msw';
export const handlers = [
// User endpoints
rest.get('/api/v1/users/:id', (req, res, ctx) => {
const { id } = req.params;
return res(
ctx.status(200),
ctx.json({
id: Number(id),
name: 'John Doe',
email: 'john@example.com'
})
);
}),
rest.post('/api/v1/users', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.status(201),
ctx.json({
id: 123,
...body
})
);
}),
// Auth endpoints
rest.post('/auth/login', async (req, res, ctx) => {
const { email, password } = await req.json();
if (email === 'test@example.com' && password === 'password123') {
return res(
ctx.status(200),
ctx.json({ access_token: 'fake-token' })
);
}
return res(
ctx.status(401),
ctx.json({ error: 'Invalid credentials' })
);
}),
];
src/mocks/server.ts:
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
Testing Context
// src/contexts/AuthContext.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AuthProvider, useAuth } from './AuthContext';
function TestComponent() {
const { user, login, logout } = useAuth();
return (
<div>
{user ? (
<>
<p>Logged in as {user.name}</p>
<button onClick={logout}>Logout</button>
</>
) : (
<button onClick={() => login('test@example.com', 'password')}>
Login
</button>
)}
</div>
);
}
describe('AuthContext', () => {
it('provides authentication state', async () => {
const user = userEvent.setup();
render(
<AuthProvider>
<TestComponent />
</AuthProvider>
);
// Initially not logged in
expect(screen.queryByText(/logged in as/i)).not.toBeInTheDocument();
// Login
await user.click(screen.getByText(/login/i));
// After login
await waitFor(() => {
expect(screen.getByText(/logged in as/i)).toBeInTheDocument();
});
});
});
Snapshot Testing
Use com moderação - snapshots ficam desatualizados facilmente.
it('matches snapshot', () => {
const { container } = render(<UserProfile user={mockUser} />);
expect(container).toMatchSnapshot();
});
// Atualizar snapshots
// npm test -- -u
Accessibility Testing
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('has no accessibility violations', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Comandos Úteis
# Rodar todos os testes
npm test
# Watch mode
npm test -- --watch
# Coverage
npm test -- --coverage
# Específico
npm test -- UserProfile
# Update snapshots
npm test -- -u
# Verbose
npm test -- --verbose
Best Practices
✅ Fazer
- Usar
getByRolequando possível (melhor para a11y) - Testar comportamento do usuário, não implementação
- Mock apenas APIs externas (não componentes)
- Queries em ordem de prioridade: Role > Label > PlaceholderText > TestId
- Async queries com
waitForoufindBy*
❌ Evitar
- Testar detalhes de implementação (state interno, props)
- Snapshots excessivos
getByTestIdcomo primeira opção- Múltiplos
act()warnings - Testes acoplados