Série Fullstack - 11: Testes no Frontend com React Testing Library
E aí, pessoal! Nesta parte 11 da série Fullstack com Node.js, React e GraphQL vamos adicionar testes no frontend da aplicação usando a lib React Testing Library.
Pirâmide ou Troféu?
Um consenso na comunidade de desenvolvimento de software é dividir os testes automatizados em forma de pirâmide:
Pirâmide de testes
Temos menos testes E2E (end-to-end), que testam a aplicação de ponta-a-ponta, integrando frontend e backend, pois são mais lentos para rodar e mais difíceis de manter.
No meio temos testes de integração, que testam componentes de forma integrada, mas mockando (ou simulando) partes mais lentas como chamadas de rede.
Na base, testes unitários, que testam componentes de forma isolada, sendo mais rápido para executar, portanto permitindo termos mais destes testes.
Esta estrutura faz muito sentido, mas ela se baseia em uma premissa de que os testes mais amplos (integração e E2E) são mais custosos e lentos. No entanto, as ferramentas estão evoluindo e derrubando esta premissa em muitos casos.
Por isso, o Kent C. Dodds (autor do React Testing Library) propõe uma distribuição diferente, o Troféu de Testes:
Troféu de testes
Aqui há uma deformação na pirâmide, deixando a parte de testes de integração bem maior, pois, com ferramentas como o RTL (React Testing Library), são relativamente rápidos de escrever e executar, e trazem mais confiança do que testes unitários.
Na base ele adiciona testes estáticos, com ferramentas como ESLint e TypeScript, que verificam seu código antes mesmo de executar e ajudam a prevenir bugs.
Todo o racional por trás desta proposta está no artigo: Write tests. Not too many. Mostly integration. (Escreva testes. Não muitos. Mais de integração.)
Vamos seguir esta ideia aqui na nossa aplicação.
Setup das ferramentas
No projeto de frontend vamos instalar as dependências de desenvolvimento:
npm i -D jest @types/jest msw @testing-library/react @testing-library/jest-dom
Instalamos o Jest, que é o framework que vai executar os testes, o MSW (Mock Service Worker) para mockar as chamadas de backend, e o próprio RTL, juntamente o jest-dom para facilitar na verificação dos elementos na tela.
Mockando o backend
Nos testes de integração nós não vamos acessar o backend, mas vamos usar o MSW para simular a resposta do servidor. Isso torna a execução mais rápida e permite simular diferentes respostas.
Vamos criar um um servidor GraphQL fake no arquivo /src/tests/server.ts:
import { graphql } from 'msw'
import { setupServer } from 'msw/node'
const handlers = [
graphql.query('HomePageQuery', (req, res, ctx) => {
return res(
ctx.data({
investments: [
{
balance: 100,
invested: 50,
},
{
balance: 200,
invested: 100,
},
],
})
)
}),
graphql.query('ChartQuery', (req, res, ctx) => {
return res(
ctx.data({
// dados para testes
})
)
}),
graphql.query('InvestmentsPageQuery', (req, res, ctx) => {
return res(
ctx.data({
// dados para testes
})
)
}),
]
export const server = setupServer(...handlers)
Veja que definimos handlers com resposta padrão para cada query que nossa aplicação possui, mas podemos sobrescrever estas respostas em testes específicos para testar diferentes cenários.
No arquivo /src/tests/setup.ts vamos adicionar chamadas para iniciar o servidor fake antes dos testes (beforeAll
), resetar os handlers após cada teste (afterEach
) e desligar o servidor ao final de todos os testes (afterAll
):
import 'regenerator-runtime/runtime'
import '@testing-library/jest-dom'
import { server } from './server'
beforeAll(() => {
server.listen()
})
afterEach(() => {
server.resetHandlers()
})
afterAll(() => {
server.close()
})
Por fim adicionamos na configuração do Jest (jest-config.js) a instrução para ele executar este arquivo de setup:
module.exports = {
setupFilesAfterEnv: ['./src/tests/setup.ts'],
coveragePathIgnorePatterns: ['/node_modules/', '/tests/'],
}
Escrevendo os testes
Antes de começar, de fato, a escrever os testes, vamos criar um componente utilitário para adicionar os providers ao componente que estivermos testando, para conseguir fazer o render destes componentes sem problemas:
// src/tests/utils.tsx
import React, { PropsWithChildren, ReactElement } from 'react'
import { render } from '@testing-library/react'
import {
ApolloClient,
ApolloProvider,
HttpLink,
InMemoryCache,
} from '@apollo/client'
import fetch from 'cross-fetch'
import { ThemeProvider } from 'styled-components'
import { theme } from '../theme'
import { MuiThemeProvider } from '@material-ui/core'
const client = new ApolloClient({
link: new HttpLink({
uri: 'http://localhost:5000/graphql',
fetch,
}),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
},
})
const Providers = ({ children }: PropsWithChildren<{}>) => {
return (
<ApolloProvider client={client}>
<MuiThemeProvider theme={theme}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</MuiThemeProvider>
</ApolloProvider>
)
}
const customRender = (ui: ReactElement, options = {}) => {
return render(ui, { wrapper: Providers, ...options })
}
// re-export everything
export * from '@testing-library/react'
// override render method
export { customRender as render }
Explicando:
Criamos uma instância do ApolloClient com o cache desabilitado para um teste não interferir no outro. Também passamos o fetch
da biblioteca cross-fetch, para que o MSW consiga interceptar as chamadas.
Criamos um componente Providers
, para adicionar todos os providers.
Por fim re-exportamos as funções do RTL com o render
customizado para adicionar o Providers
em todos os componentes testados.
Agora sim, escrevendo os testes
Seguindo a ideia de escrever mais testes de integração, vamos testar cada página (Home e Investimentos) e testar a navegação principal através do componente App.
Testes da Home:
import React from 'react'
import { graphql } from 'msw'
import { render, screen } from '../utils'
import HomePage from '../../pages/HomePage'
import { server } from '../server'
describe('Home Page', () => {
describe('when data is loading', () => {
it('shows loading message', () => {
// given
render(<HomePage />)
// then
expect(screen.getByText('Carregando...')).toBeVisible()
})
})
describe('when data loads successfully', () => {
it('shows cards', async () => {
// given
render(<HomePage />)
await screen.findByTestId('card-patrimony')
// then
expect(screen.getByTestId('card-patrimony')).toHaveTextContent('R$300.00')
expect(screen.getByTestId('card-profit-percent')).toHaveTextContent(
'100%'
)
expect(screen.getByTestId('card-profit')).toHaveTextContent('R$150.00')
})
it('shows chart', async () => {
// given
render(<HomePage />)
await screen.findByTestId('chart')
// then
expect(screen.getByTestId('chart')).toBeVisible()
})
})
describe('when data fails to load', () => {
it('shows an error message', async () => {
// given
server.use(
graphql.query('HomePageQuery', (req, res, ctx) => {
return res(ctx.status(403))
})
)
render(<HomePage />)
const errorElement = await screen.findByTestId('error')
// then
expect(errorElement).toBeVisible()
})
})
})
Testamos cada cenário de retorno do servidor: dados carregando, dados carregados com sucesso e falha no carregamento.
Com o MSW, o render inicialmente traz o componente em estado "carregando". Para verificar o componente com os dados carregados, precisamos fazer um await
em um dos elementos da tela:
render(<HomePage />)
await screen.findByTestId('card-patrimony')
Veja que usamos muitos getByTestId
para selecionar elementos na tela pelo atributo data-testid
. Esta é outra recomendação do RTL. Usar um atributo específico para testes é mais confiável do que usar classes, por exemplo, que podem mudar de acordo com o CSS.
Para testar o estado de erro, sobrescrevemos o handler do MSW dentro do teste, para simular o erro no servidor:
server.use(
graphql.query('HomePageQuery', (req, res, ctx) => {
return res(ctx.status(403))
})
)
Os outros testes são muito parecidos. Você pode conferir no código final.
Cobertura
Com estes testes conseguimos uma boa cobertura de quase 100% em todo o código:
Cobertura dos testes
Integração com Travis CI
Pra terminar, vamos adicionar as verificações de frontend ao Travis CI.
Vamos separar as verificações de frontend e backend em dois jobs que rodam em paralelo. Nosso .travis.yml fica assim:
language: node_js
node_js:
- '12.18.1'
cache:
directories:
- 'node_modules'
services:
- postgresql
jobs:
include:
- before_install:
- cd backend
before_script:
- cp config/database.ci.js config/database.js
- psql -c 'create database mymoney_ci_test;' -U postgres
script:
- npm run prettier:check
- npm run lint
- npm run test
- before_install:
- cd frontend
script:
- npm run prettier:check
- npm run check-types
- npm run lint
- npm run test
Resultado final
O código do projeto até este ponto está em: https://github.com/doug2k1/my-money/tree/v11.0.0
No próximo capítulo
Na próxima parte vamos mandar nossa aplicação para produção!
Stay tuned!
Tags: frontend|full-stack|react|testes