Testes em React 2026: Vitest, React Testing Library e Melhores Práticas

Dominar os testes em React com Vitest e React Testing Library. Aprender padrões de teste de componentes, tratamento assíncrono, estratégias de mocking e melhores práticas para entrevistas técnicas em 2026.

Diagrama de testes em React 2026 com Vitest e React Testing Library

Os testes em React durante 2026 se concentram no Vitest como o executor de testes dominante e no React Testing Library (RTL) como o padrão para testes de componentes. Essa combinação oferece ciclos de feedback rápidos, suporte nativo a módulos ES e testes que verificam comportamento ao invés de detalhes de implementação.

Filosofia de Testes

Escrever testes que se assemelhem a como os usuários interagem com os componentes. Consultar por roles acessíveis, labels e texto—não por classes CSS ou atributos data-testid. Essa abordagem detecta bugs reais e sobrevive ao refactoring.

Configuração do Vitest para Projetos React

O Vitest substituiu o Jest como o executor de testes preferido para aplicações React modernas. O suporte nativo a módulos ES elimina a sobrecarga de transformação, e a integração próxima com o Vite significa que a configuração espelha o ambiente de desenvolvimento.

Uma configuração do Vitest pronta para produção lida com a transformação JSX, aliases de caminhos e configuração do ambiente de testes sem boilerplate:

vitest.config.tstypescript
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    // Use jsdom for DOM APIs and React rendering
    environment: 'jsdom',
    // Run setup file before each test file
    setupFiles: ['./src/test/setup.ts'],
    // Include only test files, exclude e2e
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    // Enable global test APIs (describe, it, expect)
    globals: true,
    // Generate coverage reports
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      exclude: ['node_modules', 'src/test/**'],
    },
  },
})

O arquivo de setup estende os matchers e configura a limpeza entre testes:

src/test/setup.tstypescript
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'

// Clean up rendered components after each test
afterEach(() => {
  cleanup()
})

// Mock window.matchMedia for responsive components
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation((query: string) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
})

Essa configuração fornece uma base limpa para cada teste e lida com os mocks comuns de APIs do navegador que o jsdom não possui.

Testes de Componentes com React Testing Library

O React Testing Library impõe testar componentes da perspectiva do usuário. Ao invés de inspecionar estado interno ou valores de props, os testes interagem com a saída renderizada através de consultas de acessibilidade.

Considere um componente de busca que filtra resultados e exibe estados de carregamento:

SearchResults.tsxtsx
import { useState } from 'react'

interface SearchResultsProps {
  onSearch: (query: string) => Promise<string[]>
}

export function SearchResults({ onSearch }: SearchResultsProps) {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<string[]>([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function handleSearch() {
    if (!query.trim()) return
    setIsLoading(true)
    setError(null)
    try {
      const data = await onSearch(query)
      setResults(data)
    } catch (e) {
      setError('Search failed. Please try again.')
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <div>
      <label htmlFor="search-input">Search</label>
      <input
        id="search-input"
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <button onClick={handleSearch} disabled={isLoading}>
        {isLoading ? 'Searching...' : 'Search'}
      </button>
      {error && <p role="alert">{error}</p>}
      <ul aria-label="Search results">
        {results.map((result, i) => (
          <li key={i}>{result}</li>
        ))}
      </ul>
    </div>
  )
}

Os testes verificam o comportamento do componente através das interações do usuário e estados assíncronos:

SearchResults.test.tsxtypescript
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { SearchResults } from './SearchResults'

describe('SearchResults', () => {
  it('displays results after successful search', async () => {
    // Arrange: mock the search function
    const mockSearch = vi.fn().mockResolvedValue(['React', 'Vue', 'Angular'])
    const user = userEvent.setup()
    render(<SearchResults onSearch={mockSearch} />)

    // Act: type a query and click search
    await user.type(screen.getByLabelText('Search'), 'framework')
    await user.click(screen.getByRole('button', { name: 'Search' }))

    // Assert: results appear in the list
    expect(await screen.findByText('React')).toBeInTheDocument()
    expect(screen.getByText('Vue')).toBeInTheDocument()
    expect(mockSearch).toHaveBeenCalledWith('framework')
  })

  it('shows loading state during search', async () => {
    // Create a promise that doesn't resolve immediately
    let resolveSearch: (value: string[]) => void
    const searchPromise = new Promise<string[]>((resolve) => {
      resolveSearch = resolve
    })
    const mockSearch = vi.fn().mockReturnValue(searchPromise)
    const user = userEvent.setup()
    render(<SearchResults onSearch={mockSearch} />)

    await user.type(screen.getByLabelText('Search'), 'test')
    await user.click(screen.getByRole('button', { name: 'Search' }))

    // Button shows loading state
    expect(screen.getByRole('button', { name: 'Searching...' })).toBeDisabled()

    // Resolve the search to clean up
    resolveSearch!([])
  })

  it('displays error message on search failure', async () => {
    const mockSearch = vi.fn().mockRejectedValue(new Error('Network error'))
    const user = userEvent.setup()
    render(<SearchResults onSearch={mockSearch} />)

    await user.type(screen.getByLabelText('Search'), 'query')
    await user.click(screen.getByRole('button', { name: 'Search' }))

    // Error message appears with alert role for screen readers
    expect(await screen.findByRole('alert')).toHaveTextContent(
      'Search failed. Please try again.'
    )
  })
})

A biblioteca userEvent simula interações de usuário realistas incluindo eventos de teclado e gerenciamento de foco, detectando bugs que eventos sintéticos não capturam.

Padrões de Testes Assíncronos e waitFor

Operações assíncronas requerem estratégias de espera explícitas. O React Testing Library fornece consultas findBy que combinam getBy com espera automática, além de waitFor para asserções complexas.

Prioridade de Consultas

Preferir findBy sobre waitFor + getBy ao esperar elementos aparecerem. Reservar waitFor para asserções sobre elementos existentes que mudam de estado.

Padrões assíncronos comuns aparecem ao testar hooks e componentes de busca de dados:

async-patterns.test.tsxtypescript
import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'

// Wait for element to appear (preferred for new elements)
it('loads user profile on mount', async () => {
  render(<UserProfile userId="123" />)
  
  // findBy returns a promise, automatically waits up to 1000ms
  const heading = await screen.findByRole('heading', { name: /john doe/i })
  expect(heading).toBeInTheDocument()
})

// Wait for element to disappear
it('removes loading spinner after data loads', async () => {
  render(<DataTable />)
  
  // Wait for spinner to be removed from DOM
  await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'))
  expect(screen.getByRole('table')).toBeInTheDocument()
})

// Wait for multiple conditions
it('enables submit button when form is valid', async () => {
  const user = userEvent.setup()
  render(<RegistrationForm />)

  const submitButton = screen.getByRole('button', { name: 'Submit' })
  expect(submitButton).toBeDisabled()

  await user.type(screen.getByLabelText('Email'), 'user@example.com')
  await user.type(screen.getByLabelText('Password'), 'SecurePass123!')

  // waitFor retries the assertion until it passes or times out
  await waitFor(() => {
    expect(submitButton).toBeEnabled()
  })
})

// Avoid false positives with queryBy for absence checks
it('does not show premium badge for free users', async () => {
  render(<UserCard subscription="free" />)
  
  // queryBy returns null instead of throwing, use for absence checks
  expect(screen.queryByText('Premium')).not.toBeInTheDocument()
})

A personalização de timeouts ajuda com operações lentas sem desacelerar toda a suite:

typescript
// Increase timeout for slow operations
await screen.findByText('Upload complete', {}, { timeout: 5000 })

await waitFor(
  () => expect(mockSubmit).toHaveBeenCalled(),
  { timeout: 3000, interval: 100 }
)

Pronto para mandar bem nas entrevistas de React / Next.js?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Estratégias de Mocking: Módulos, APIs e Hooks

As capacidades de mocking do Vitest lidam com dependências externas sem poluir o grafo de módulos. O mocking estratégico isola componentes de chamadas de rede, bibliotecas de terceiros e dependências complexas.

O mocking de módulos substitui imports no nível do bundler:

api.test.tstypescript
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUserData } from './api'

// Mock the entire fetch module
vi.mock('./http-client', () => ({
  httpClient: {
    get: vi.fn(),
  },
}))

import { httpClient } from './http-client'

describe('fetchUserData', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('transforms API response into user model', async () => {
    // Type-safe mock implementation
    vi.mocked(httpClient.get).mockResolvedValue({
      data: { id: 1, first_name: 'John', last_name: 'Doe' },
    })

    const user = await fetchUserData(1)

    expect(user).toEqual({
      id: 1,
      fullName: 'John Doe',
    })
  })
})

Para hooks React que dependem de contexto ou estado externo, o utilitário renderHook permite testes isolados:

useAuth.test.tstypescript
import { renderHook, waitFor } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { useAuth } from './useAuth'
import { AuthProvider } from './AuthContext'

describe('useAuth', () => {
  it('returns authenticated user after login', async () => {
    const mockLogin = vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
    
    const { result } = renderHook(() => useAuth(), {
      wrapper: ({ children }) => (
        <AuthProvider loginFn={mockLogin}>{children}</AuthProvider>
      ),
    })

    expect(result.current.user).toBeNull()
    expect(result.current.isAuthenticated).toBe(false)

    await result.current.login('alice@example.com', 'password')

    await waitFor(() => {
      expect(result.current.user).toEqual({ id: 1, name: 'Alice' })
      expect(result.current.isAuthenticated).toBe(true)
    })
  })
})

MSW (Mock Service Worker) fornece mocking de API no nível da rede, permitindo testes que exercitam chamadas fetch reais:

handlers.tstypescript
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Test User',
    })
  }),

  http.post('/api/login', async ({ request }) => {
    const body = await request.json()
    if (body.email === 'valid@example.com') {
      return HttpResponse.json({ token: 'abc123' })
    }
    return HttpResponse.json(
      { error: 'Invalid credentials' },
      { status: 401 }
    )
  }),
]

Testando Server Components do React 19

Server Components apresentam desafios únicos de teste, já que são executados no servidor e transmitem HTML para o cliente. Testes unitários diretos requerem uma abordagem diferente dos testes de componentes cliente.

Para Server Components que realizam busca de dados, testar a saída de renderização com fontes de dados mockadas:

ServerComponent.test.tsxtypescript
import { describe, it, expect, vi } from 'vitest'

// Mock the data fetching function
vi.mock('./db', () => ({
  getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Server User' }),
}))

import { render, screen } from '@testing-library/react'
import { UserProfile } from './UserProfile.server'

describe('UserProfile Server Component', () => {
  it('renders user data from database', async () => {
    // Server components are async, await the component
    const Component = await UserProfile({ userId: '1' })
    render(Component)

    expect(screen.getByRole('heading')).toHaveTextContent('Server User')
  })
})

Testes de integração com Playwright fornecem a cobertura mais confiável para Server Components ao testar o pipeline de renderização completo:

e2e/server-component.spec.tstypescript
import { test, expect } from '@playwright/test'

test('server component renders with streamed data', async ({ page }) => {
  await page.goto('/users/1')
  
  // Wait for streaming to complete
  await expect(page.getByRole('heading', { name: /profile/i })).toBeVisible()
  await expect(page.getByText('Server User')).toBeVisible()
})

Testando Custom Hooks com Estado Complexo

Hooks personalizados que gerenciam estado complexo, efeitos colaterais ou assinaturas externas se beneficiam de testes dedicados. O utilitário renderHook do React Testing Library lida com o ciclo de vida e atualizações dos hooks.

Considere um hook de busca com debounce que se coordena com uma API:

useDebounceSearch.tstypescript
import { useState, useEffect, useCallback } from 'react'

export function useDebounceSearch<T>(
  searchFn: (query: string) => Promise<T[]>,
  delay = 300
) {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<T[]>([])
  const [isLoading, setIsLoading] = useState(false)

  useEffect(() => {
    if (!query) {
      setResults([])
      return
    }

    setIsLoading(true)
    const timeoutId = setTimeout(async () => {
      try {
        const data = await searchFn(query)
        setResults(data)
      } finally {
        setIsLoading(false)
      }
    }, delay)

    return () => clearTimeout(timeoutId)
  }, [query, searchFn, delay])

  return { query, setQuery, results, isLoading }
}

Os testes verificam o comportamento do debounce e as transições de estado:

useDebounceSearch.test.tstypescript
import { renderHook, act, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useDebounceSearch } from './useDebounceSearch'

describe('useDebounceSearch', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  it('debounces search calls', async () => {
    const mockSearch = vi.fn().mockResolvedValue(['result1', 'result2'])
    const { result } = renderHook(() => useDebounceSearch(mockSearch, 300))

    // Type multiple characters quickly
    act(() => result.current.setQuery('r'))
    act(() => result.current.setQuery('re'))
    act(() => result.current.setQuery('rea'))
    act(() => result.current.setQuery('reac'))
    act(() => result.current.setQuery('react'))

    // Search not called yet (debouncing)
    expect(mockSearch).not.toHaveBeenCalled()

    // Advance timers past debounce delay
    await act(async () => {
      vi.advanceTimersByTime(300)
    })

    // Only one search call with final query
    expect(mockSearch).toHaveBeenCalledTimes(1)
    expect(mockSearch).toHaveBeenCalledWith('react')
  })

  it('shows loading state during search', async () => {
    let resolveSearch: (value: string[]) => void
    const mockSearch = vi.fn().mockImplementation(
      () => new Promise((resolve) => { resolveSearch = resolve })
    )
    const { result } = renderHook(() => useDebounceSearch(mockSearch, 100))

    act(() => result.current.setQuery('test'))
    
    await act(async () => {
      vi.advanceTimersByTime(100)
    })

    expect(result.current.isLoading).toBe(true)

    await act(async () => {
      resolveSearch(['result'])
    })

    expect(result.current.isLoading).toBe(false)
    expect(result.current.results).toEqual(['result'])
  })
})

Os fake timers do Vitest permitem testes determinísticos do comportamento dependente de tempo sem atrasos reais.

Perguntas de Entrevista sobre Testes em React

Entrevistas técnicas frequentemente investigam conhecimentos de testes. Essas perguntas avaliam a compreensão prática ao invés de conceitos teóricos.

Armadilha Comum

Evitar testar detalhes de implementação como valores de estado ou métodos de componentes. Testes que quebram ao refatorar a estrutura interna fornecem falsa confiança e carga de manutenção.

Prioridade de consultas no React Testing Library

A prioridade de consulta recomendada segue acessibilidade: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId. Consultas baseadas em roles garantem que os componentes permaneçam acessíveis e os testes sobrevivam a mudanças de markup.

Quando usar findBy vs waitFor

Consultas findBy esperam elementos aparecerem no DOM e retornam uma promise. Usá-las quando espera-se novos elementos após operações assíncronas. waitFor envolve asserções que podem não passar imediatamente, tentando novamente até sucesso ou timeout. Usar waitFor para asserções sobre mudanças de estado em elementos existentes.

Mocking de fetch vs MSW

O mocking direto de fetch (vi.spyOn(global, 'fetch')) funciona para casos simples, mas requer reimplementar objetos Response e tratamento de erros. MSW intercepta no nível da rede, permitindo que testes exercitem chamadas fetch reais, serialização de requisições e caminhos de código de tratamento de erros.

Para prática mais aprofundada desses conceitos, explorar as perguntas de entrevista de React Testing no SharpSkill.

Conclusão

  • Configurar o Vitest com ambiente jsdom, arquivos de setup para extensões de matchers e relatórios de cobertura para visibilidade sobre lacunas de cobertura de testes
  • Consultar componentes por roles e labels acessíveis para escrever testes que verificam comportamento voltado ao usuário e detectam bugs reais
  • Usar consultas findBy para elementos que aparecem após operações assíncronas, waitFor para asserções sobre estado em mudança
  • Fazer mock no nível apropriado: mocks de módulos para isolamento unitário, MSW para testes realistas da camada de rede
  • Testar hooks personalizados com renderHook e fake timers para verificar gerenciamento de estado complexo e efeitos colaterais
  • Server Components requerem renderização assíncrona em testes ou testes de integração completos com Playwright
  • Leitura relacionada: Padrões avançados de React Hooks e TypeScript com React para padrões de teste type-safe

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#react
#testing
#vitest
#react-testing-library
#javascript

Compartilhar

Artigos relacionados