GHub
Pesquise repositórios e usuários do GitHub.
Objetivo
Motivação para criar o projeto
- Este projeto foi criado como exemplo para uma decisão técnica sobre a escolha de ferramentas de teste.
- Irei criar branchs separadas para testar diferentes implementações de bibliotecas de teste, como exemplo o Vitest, Cypress, Jest, Axios Mock Adapter, MSW, Playwright.
Tecnologias
O que foi utilizado neste exemplo?
Instalação [JEST]
Como foi instalar o Jest? Muito trabalhoso?
-
Nota do autor
- Não perca tempo seguindo os passos de instalação e setuo no site do Jest, não funciona, te induz ao erro, e faz você criar um monte de configuração em cima da inicial para ver se funciona e no final nem sabe mais o que fez dar certo.
-
Setup
-
Instale os seguintes pacotes
- jest
- ts-jest
- react-test-renderer
- @types/jest
- @testing-library/react
- @testing-library/user-event
- @testing-library/jest/dom
- @testing-library/dom
npm i -D jest typescript ts-jest @types/jest react-test-renderer @testing-library/react @testing-library/user-event @testing-library/jest-dom @testing-library/dom
-
Crie o arquivo jest.config.js
na raiz do projeto, com o seguinte conteúdo dentro:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node'
};
-
Adicione o comando de execução aos scripts
do package.json
:
{
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
}
-
Após isso, ao tentar rodar os testes, vai rolar vários erros, erro de transformação de arquivos do ts-jest, erros com path absolute com aliases, etc, etc, porque o jest é bem chato de configurar. Então vamos lá:
- Para corrigir o erro de transformação de JSX para o jest entender os componentes, vamos precisar sobrescrever uma regra
jsx
do tsconfig.json
que o next mantém como preserve
ao invés de react
, para sobrescrever a regra, crie um arquivo chamado tsconfig.jest.json
e adicione o seguinte trecho de código:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}
- Esse json extende o
tsconfig
padrão e sobrescreve a rerga jsx
que o next
obriga ser preserve
mas para o ts-jest
funcionar precisa estar configrada como react
.
-
Após criar o novo arquivo, você precisa indicar para o jest
, que o ts-jest
irá usá-lo ao invés do arquivo padrão, para isso, adicione o trecho abaixo no jest.config.js
'ts-jest': {
isolatedModules: true,
tsconfig: 'tsconfig.jest.json'
}
- Logo após, o conteúdo do arquivo deve ficar assim:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'ts-jest': {
isolatedModules: true,
tsconfig: 'tsconfig.jest.json'
}
}
}
-
Agora vamos corrigir o problema com os caminhos absolutos, o jest não entende o a aliases @/
nos imports
dos arquivos, para isso vamos criar uma entrada no objeto moduleNameMapper
que irá converter os imports com @/
para o caminho real que aponta para o arquivo:
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1'
}
-
Outro problema: imagens. Precisamos transformar as imagens para que o jest entenda e renderize os teste. Para isso vamos adicionar um arquivo chamado fileTransformer.js
na pasta test
dentro da raiz do projeto, esse arquivo deve ter o seguinte código:
const path = require('path')
module.exports = {
process(sourceText, sourcePath, options) {
return {
code: `module.exports = ${JSON.stringify(
path.basename(sourcePath)
)};`
}
}
}
-
Depois de criar esse arquivo, registre-o no jest.config.js
:
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
isolatedModules: true,
tsconfig: 'tsconfig.jest.json'
}
],
// Adicione a linha abaixo
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/test/fileTransformer.js'
},
-
Depois de configurar tudo, ganhei uns vários erros do Next.js
(router, next image, etc, muita coisa fora de ordem) e do React, e pra isso, foi necessário adicionar umas configurações adicionais no arquivo jest.setup.tsx
:
-
Importar o react de maneira global
import React from 'react'
global.React = React
-
Importar o jest-dom
para adicionar métodos de asserção ao expect
import '@testing-library/jest-dom'
-
Importar e registrar os métodos do next para o jest
const nextJest = require('next/jest')
const createJestConfig = nextJest({ dir: './' })
module.exports = createJestConfig()
-
Mockar o next router e image component
jest.mock('next/image', () => ({
__esModule: true,
default: (props: any) => {
// eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
return <img {...props} />
}
}))
jest.mock('next/router', () => ({
useRouter() {
return {
route: '/',
pathname: '',
query: '',
asPath: '',
push: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn()
},
beforePopState: jest.fn(() => null),
prefetch: jest.fn(() => null)
}
}
}))
-
No final o arquivo deve ficar assim:
/* Make react global to components inside jest */
import React from 'react'
global.React = React
/* Add assertions methods */
import '@testing-library/jest-dom'
/* Next.js setup for Jest */
const nextJest = require('next/jest')
const createJestConfig = nextJest({ dir: './' })
module.exports = createJestConfig()
/* Mock Next components and router */
jest.mock('next/image', () => ({
__esModule: true,
default: (props: any) => {
// eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
return <img {...props} />
}
}))
jest.mock('next/router', () => ({
useRouter() {
return {
route: '/',
pathname: '',
query: '',
asPath: '',
push: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn()
},
beforePopState: jest.fn(() => null),
prefetch: jest.fn(() => null)
}
}
}))
-
Após todo esse setup, consegui finalmente rodar um teste:
import { render, screen } from '@testing-library/react'
import { Navbar } from '@/components/Navbar'
import { useRouter } from 'next/router'
describe('Navbar', () => {
const renderComponent = () => render(<Navbar />)
test('Should render properly', async () => {
renderComponent()
const link = await screen.findByRole('link', { name: /GHub/i })
const logo = await screen.findByRole('img', { name: /GHub logo/i })
const searchInput = await screen.findByPlaceholderText('Pesquisar')
expect(link).toBeInTheDocument()
expect(searchInput).toBeInTheDocument()
})
})
![Primeiro teste](https://user-images.githubusercontent.com/15758789/225721733-fabd50ab-1a25-43cd-b7ff-3f8c305e3aba.png)
Considerações pós setup
Fiz mais algum setup? Precisou de ajustes? Como ficou?
Depois de todo o setup acima, eu comecei a escrever os testes de unidade, e como sempre, vários erros, ainda precisava de ajustes caso precisasse implementar os testes da maneira correta.
Um dos cenários que passei, foi testar componentes que usavam o hook useRouter()
, no setup inicial, para os testes rodarem eu havia feito um mock do router, mas aquele mock limitava os testes, e para isso eu resolvi criar um arquivo de configuração para os testes, e com isso eu criei meu próprio render, com alguns detalhes a mais.
Esse arquivo que eu criei, possui alguns recursos, ele importa e re-exporta os recursos da RTL, e ele exporta um customRender como comentei.
Vamos ao arquivo (criado em raiz do projeto
> /test/index.tsx
)
// test/index.tsx
import type { PropsWithChildren } from 'react'
import { render as rtlRender, RenderOptions } from '@testing-library/react'
import user from '@testing-library/user-event'
import { RouterContext } from 'next/dist/shared/lib/router-context'
import { createRouterMock } from './mocks/createRouterMock'
import { NextRouter } from 'next/router'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
type TestWrapperProps = {
router?: Partial<NextRouter>
}
type CustomRenderProps = {
router: Partial<NextRouter>
options?: Omit<RenderOptions, 'wrapper'>
}
const AllProviders = ({
children,
router = {}
}: PropsWithChildren<TestWrapperProps>) => (
<RouterContext.Provider value={createRouterMock(router)}>
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
</RouterContext.Provider>
)
const customRender = (ui: JSX.Element, props?: CustomRenderProps) =>
rtlRender(ui, {
wrapper: ({ children }: PropsWithChildren) => (
<AllProviders router={props?.router}>{children}</AllProviders>
),
...props?.options
})
export * from '@testing-library/react'
export { user, customRender }
O trecho a seguir, é usado para fazer o registro de todos os providers/contexts que tivermos no projeto, para que os testes e hooks funcionem corretamente (como se fosse a aplicação real rodando). Nesse exemplo eu passei o provider do roteador do Next.js e o provider do React query, para que os hooks useQuery
dentro da app funcionem corretamente.
const AllProviders = ({
children,
router = {}
}: PropsWithChildren<TestWrapperProps>) => (
<RouterContext.Provider value={createRouterMock(router)}>
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
</RouterContext.Provider>
)
Poderíamos mockar esses contexts/providers, mas nem sempre queremos mockar as coisas, e nesse caso eu quero que a aplicação funcione normalmente como se estivesse sendo usado real.
Sobre o customRender()
, ele é bem simples. Uma função que retorna o render da Testing Library, mas com algumas opções no objeto, o primeiro parâmetro (ui
), é o componente que eu quero testar, e depois eu passo um objeto, e preencho a chave wrapper
que recebe uma função passando um children
(nosso componente passado na ui) que será injetado dentro do AllProviders
e receberá todos os contextos e recursos necessários para funcionar. E adicionalmente eu recebo as outras options do RTL render para caso eu queira fazer alguma customização dentro do arquivo de teste, eu tenho acesso a interface.
const customRender = (ui: JSX.Element, props?: CustomRenderProps) =>
rtlRender(ui, {
wrapper: ({ children }: PropsWithChildren) => (
<AllProviders router={props?.router}>{children}</AllProviders>
),
...props?.options
})
O restante do arquivo é somente import e reexport dos recursos.
Vamos ao mock do next router, e aqui não tem nada de mais, é só uma factory simples:
// test/mocks/createRouterMock.ts
import { NextRouter } from 'next/router'
export function createRouterMock(router: Partial<NextRouter>): NextRouter {
return {
route: '/',
asPath: '/',
basePath: '',
pathname: '/',
defaultLocale: 'en',
query: {},
domainLocales: [],
back: jest.fn(),
push: jest
.fn()
.mockImplementation((path: string) =>
window?.history?.pushState({}, 'Test', path)
),
reload: jest.fn(),
replace: jest.fn(),
forward: jest.fn(),
prefetch: jest.fn(),
beforePopState: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn()
},
isReady: true,
isPreview: false,
isFallback: false,
isLocaleDomain: false,
...router
}
}
Tempo total dos testes
Após implementação dos testes
![Captura de Tela 2023-03-17 às 16 49 45](https://user-images.githubusercontent.com/15758789/226014780-3bfec925-43c1-407e-b3ec-6480af87f7b1.png)