Skip to content

Jest #2

@ipetinate

Description

@ipetinate

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions