Skip to content

Jest#1

Open
ipetinate wants to merge 15 commits intomainfrom
jest
Open

Jest#1
ipetinate wants to merge 15 commits intomainfrom
jest

Conversation

@ipetinate
Copy link
Copy Markdown
Owner

@ipetinate ipetinate commented Mar 16, 2023

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

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

@ipetinate ipetinate changed the title Jest journey's Jest Mar 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant