Pular para conteúdo

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:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

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 getByRole quando 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 waitFor ou findBy*

❌ Evitar

  • Testar detalhes de implementação (state interno, props)
  • Snapshots excessivos
  • getByTestId como primeira opção
  • Múltiplos act() warnings
  • Testes acoplados

Referências