ipetinate / ipetinate/ghub-frontend

Jest

Open
#2 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

# 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?

- [Next.js](https://nextjs.org/)
- [TailwindCSS](https://tailwindcss.com/)
- [HeroIcons](https://heroicons.com/)
- [HeadlessUI](https://headlessui.com/)
- [Flowbite](https://flowbite-react.com/)
- [Axios](https://axios-http.com/)
- [React Query](https://tanstack.com/query/latest)
- [Zustand](https://github.com/pmndrs/zustand)
- [Lodash](https://lodash.com/)

## 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

```shell
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:

```javascript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node'
};
```

- Adicione o comando de execução aos `scripts` do `package.json`:

```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:

```json
{
"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`

```javascript
'ts-jest': {
isolatedModules: true,
tsconfig: 'tsconfig.jest.json'
}
```

- Logo após, o conteúdo do arquivo deve ficar assim:

```javascript
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:

```javascript
moduleNameMapper: {
'@/(.*)': '/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:

```javascript
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`:

```javascript
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)$':
'/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

```javascript
import React from 'react'

global.React = React

```

- Importar o `jest-dom` para adicionar métodos de asserção ao `expect`

```javascript
import '@testing-library/jest-dom'
```

- Importar e registrar os métodos do next para o jest

```javascript
const nextJest = require('next/jest')

const createJestConfig = nextJest({ dir: './' })

module.exports = createJestConfig()
```

- Mockar o next router e image component

```javascript
jest.mock('next/image', () => ({
__esModule: true,
default: (props: any) => {
// eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
return
}
}))

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:

```javascript
/* 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
}
}))

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:

```tsx
import { render, screen } from '@testing-library/react'

import { Navbar } from '@/components/Navbar'
import { useRouter } from 'next/router'

describe('Navbar', () => {
const renderComponent = () => render()

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`)

```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
}
type CustomRenderProps = {
router: Partial
options?: Omit
}

const AllProviders = ({
children,
router = {}
}: PropsWithChildren) => (


{children}


)

const customRender = (ui: JSX.Element, props?: CustomRenderProps) =>
rtlRender(ui, {
wrapper: ({ children }: PropsWithChildren) => (
{children}
),
...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.

```tsx
const AllProviders = ({
children,
router = {}
}: PropsWithChildren) => (


{children}


)
```

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.

```tsx
const customRender = (ui: JSX.Element, props?: CustomRenderProps) =>
rtlRender(ui, {
wrapper: ({ children }: PropsWithChildren) => (
{children}
),
...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:

```ts
// test/mocks/createRouterMock.ts

import { NextRouter } from 'next/router'

export function createRouterMock(router: Partial): 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)

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.