Skip to content

Instantly share code, notes, and snippets.

@NerOcrO
Last active December 30, 2024 14:31
Show Gist options
  • Save NerOcrO/20d4aba3467d191db6048cc2a921a7f1 to your computer and use it in GitHub Desktop.
Save NerOcrO/20d4aba3467d191db6048cc2a921a7f1 to your computer and use it in GitHub Desktop.
vitest rtl test

Tips

user-event est très lent

  • fireEvent dispatches DOM events, whereas user-event simulates full interactions, which may fire multiple events and do additional checks along the way.
import '@testing-library/jest-dom'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
import { MemoryRouter } from 'react-router'

import SubmitButton from 'components/layout/SubmitButton/SubmitButton'
import ThumbnailDialog from 'components/pages/Offers/Offer/Thumbnail/ThumbnailDialog'

describe('un seul composant', () => {
  for (let index = 0; index < 500; index++) {
    it('should ' + index, () => {
      // Given
      render(
        <SubmitButton isLoading={false}>
          {'Enregistrer'}
        </SubmitButton>
      )

      // When
      userEvent.click(screen.getByText('Enregistrer')) // 4s
      // fireEvent.click(screen.getByText('Enregistrer')) // 3.1s

      // Then
      expect(true).toBe(true)
    })
  }
})

describe('plusieurs composants enfant', () => {
  for (let index = 0; index < 50; index++) {
    it('should ' + index, () => {
      // Given
      <MemoryRouter>
        <ThumbnailDialog
          setIsModalOpened={jest.fn()}
          setPreview={jest.fn()}
          setThumbnailInfo={jest.fn()}
        />
      </MemoryRouter>

      // When
      userEvent.click(screen.getByLabelText('Importer une image depuis l’ordinateur')) // 65s
      // fireEvent.click(screen.getByLabelText('Importer une image depuis l’ordinateur')) // 49s

      // Then
      expect(true).toBe(true)
    })
  }
})

Rappel

  • const func = jest.fn() simule une fonction et on s'en fiche du résultat
  • const func = jest.fn(() => true) simule une fonction qui retourne true
  • Ne pas oublier de remettre à l'état initial un mock entre chaque test

Classique (qui exporte un objet type axios, Date...)

  • jest.spyOn(axios, 'get').mockReturnValue(data) retourne des data
  • jest.spyOn(props.history, 'push').mockReturnValue(jest.fn())
  • jest.spyOn(Date, 'now').mockReturnValue(1643566484898) pour fixer la date de maintenant
  • pour fixer une date :
// Dans jest.config.ts
fakeTimers: { now: 1664703388050 },

ou

class MockDate extends Date {
  constructor() {
    super(date)
  }
}

// @ts-ignore
global.Date = MockDate

Une classe

it('se connecte au SFTP', async () => {
  // GIVEN
  jest.spyOn(Client.prototype, 'connect').mockImplementation((): any => jest.fn())
  const sftpDownloadDataSource = new FinessSftpDownloadRawData()

  // WHEN
  await sftpDownloadDataSource.exécute()

  // THEN
  expect(Client.prototype.connect).toHaveBeenCalledWith({...})
})

Une fonction

import * as action from './action'

it('stuber une fonction', () => {
  // GIVEN
  vi.spyOn(action, 'modifierUnReferentielAction').mockResolvedValueOnce()

  // WHEN

  // THEN
})

Élément du DOM

componentDidMount() {
  const script = document.createElement('script')
  script.src = '//js.scripts.com/bundle-min.js'
  document.querySelector('form').appendChild(script)
}
beforeEach(() => {
  jest.spyOn(document, 'querySelector').mockReturnValue({
    appendChild: jest.fn(),
  })
})

it('should ...', () => {
  // given
  const script = document.createElement('script')
  script.src = '//js.scripts.com/bundle-min.js'

  // when
  // Montage du composant

  // then
  expect(document.querySelector('form').appendChild).toHaveBeenCalledWith(script)
})

Un peu de tout

// Ne pas utilier jest.mock() en en-tête de fichier car cela veut dire qu'il y a du couplage fort.

async function exemplesDeMocks(navigator: Navigator) {
  navigator.geolocation.getCurrentPosition((position) => {
    console.log(position)
    history.go(1)
  })
  const response = await fetch('une/url', { method: 'POST' })
  const data = await response.json()
  const itemFromSession = sessionStorage.getItem('login')

  return {
    data,
    href: location.href,
    itemFromSession,
    varEnv: process.env.NODE_ENV,
  }
}

describe('test plein de truc', () => {
  it.only('test plein de truc', async () => {
    // GIVEN
    // jest.spyOn pour les fonctions d'un objet
    jest.spyOn(history, 'go').mockImplementationOnce(jest.fn())
    jest.spyOn(console, 'log').mockImplementationOnce(jest.fn())
    jest.spyOn(sessionStorage.__proto__, 'getItem').mockReturnValueOnce('NerOcrO')
    // @ts-ignore
    jest.spyOn(global, 'fetch').mockResolvedValueOnce({ json: () => Promise.resolve({}) })
    const position = {
      coords: {
        accuracy: 9075.79126982149,
        altitude: null,
        altitudeAccuracy: null,
        heading: null,
        latitude: 1,
        longitude: 2,
        speed: null,
      },
      timestamp: 1670251498462,
    }
    const mockedNavigator: Navigator = {
      ...navigator,
      geolocation: {
        ...navigator.geolocation,
        getCurrentPosition: (success: PositionCallback) => success(position),
      },
    }
    // Object.defineProperty pour les attributs d'un objet
    Object.defineProperty(window, 'location', {
      value: { href: 'local' },
      writable: true,
    })
    // Mieux vaut injecter les variables d'environnements en dépendance
    // @ts-ignore
    process.env.NODE_ENV = 'exemple'

    // WHEN
    const result = await exemplesDeMocks(mockedNavigator)

    // THEN
    expect(history.go).toHaveBeenCalledWith(1)
    expect(console.log).toHaveBeenCalledWith(position)
    expect(global.fetch).toHaveBeenCalledWith('une/url', { method: 'POST' })
    expect(sessionStorage.getItem).toHaveBeenCalledWith('login')
    expect(result).toStrictEqual({
      data: {},
      href: 'local',
      itemFromSession: 'NerOcrO',
      varEnv: 'exemple',
    })
    // Ne fonctionne pas et c'est normal car getCurrentPosition() n'est pas un mock mais un fake.
    // expect(mockedNavigator.geolocation.getCurrentPosition).toHaveBeenCalled()
  })
})

Implémentation

export const fonctionQuiLanceUneException = (fn: Function): number => {
  try {
    fn()
    return 1
  } catch (error) {
    return 2
  }
}

export const fonctionQuiLanceVraimentUneException = (trigger: boolean = true) => () => {
  if (trigger) {
    throw new Error('toto')
  } else {
    console.log('pas boum')
  }
}

Tests

it('vérifie que lerreur nest pas lancée', () => {
  // given
  // pour ne pas afficher le rendu dans le test
  jest.spyOn(console, 'log').mockImplementation(jest.fn())

  // when
  const result = fonctionQuiLanceUneException(fonctionQuiLanceVraimentUneException(false))

  // then
  expect(result).toBe(1)
})

it('vérifie que lerreur est attrapée', () => {
  // when
  const result = fonctionQuiLanceUneException(fonctionQuiLanceVraimentUneException())

  // then
  expect(result).toBe(2)
})

it('vérifie le message', () => {
  try {
    fonctionQuiLanceVraimentUneException()()
    // ça n'est pas ce message dans le toBe car l'erreur de ma fonction est appelée avant
    throw new Error('ne devrait pas passer ici')
  } catch (error) {
    expect(error.message).toBe('toto')
  }
})

it('vérifie que lerreur est lancée', () => {
  // c'est eslint qui me demande de remplir toThrow alors que ça ne fait pas la différence
  expect(() => fonctionQuiLanceVraimentUneException()()).toThrow('')
})

Implémentation

export const handleEditPasswordSubmit = async (
  formValues,
  handleSubmitFail,
  handleSubmitSuccess
) => {
  try {
    const response = await fetch(`${API_URL}/users/current/change-password`, {
      body: JSON.stringify(formValues),
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
      },
      method: 'POST',
    })

    if (response.status === 400) {
      handleSubmitFail(await response.json())
    } else if (response.ok === false) {
      throw new Error(`Status: ${response.status}, Status text: ${response.statusText}`)
    } else {
      handleSubmitSuccess()
    }
  } catch (error) {
    throw new Error(error)
  }
}

Tests avec jest-fetch-mock

Tests sans

import { handleEditPasswordSubmit } from '../handleEditPasswordSubmit'
import * as config from 'utils/config'

const formValues = {}
const handleSubmitFail = jest.fn()
const handleSubmitSuccess = jest.fn()

describe('when response from API', () => {
  beforeEach(() => {
    jest.spyOn(config, 'API_URL').mockReturnValue('my-localhost')
  })

  it('should call fetch properly', async () => {
    // Given
    jest.spyOn(global, 'fetch').mockResolvedValue({
      json: () => Promise.resolve({}),
    })

    // when
    await handleEditPasswordSubmit(formValues, handleSubmitFail, handleSubmitSuccess)

    // Then
    expect(global.fetch).toHaveBeenCalledWith(
      'my-localhost/users/current/change-password',
      {
        body: JSON.stringify(formValues),
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        method: 'POST',
      }
    )
  })

  it('should call success function', async () => {
    // Given
    jest.spyOn(global, 'fetch').mockResolvedValue({
      json: () => Promise.resolve({}),
      ok: true,
    })

    // when
    await handleEditPasswordSubmit(formValues, handleSubmitFail, handleSubmitSuccess)

    // Then
    expect(handleSubmitSuccess).toHaveBeenCalledTimes(1)
  })
})

describe('when status is 400', () => {
  it('should call fail function', async () => {
    // Given
    jest.spyOn(global, 'fetch').mockResolvedValue({
      json: () => Promise.resolve({ errors: ['error message']}),
      status: 400,
    })

    // when
    await handleEditPasswordSubmit(formValues, handleSubmitFail, handleSubmitSuccess)

    // Then
    expect(handleSubmitFail).toHaveBeenCalledWith({ errors: ['error message']})
  })
})

describe('when status is other that 400', () => {
  it('should stop the app', async () => {
    // Given
    jest.spyOn(global, 'fetch').mockResolvedValue({
      json: () => Promise.resolve({}),
      ok: false,
      status: 582,
      statusText: 'Error',
    })

    try {
      // When
      await handleEditPasswordSubmit(formValues, handleSubmitFail, handleSubmitSuccess)
      throw new Error('ne devrait pas passer par ici')
    }
    catch (erreur) {
      // Then
      expect(erreur).toBeInstanceOf([TON_EXCEPTION_400])
    }
  })
})

describe('when no response from API', () => {
  it('should stop the app', async () => {
    // Given
    jest.spyOn(global, 'fetch').mockRejectedValue(new Error('API is down'))

    try {
      // When
      await handleEditPasswordSubmit(formValues, handleSubmitFail, handleSubmitSuccess)
      throw new Error('ne devrait pas passer par ici')
    }
    catch (erreur) {
      // Then
      expect(erreur.message).toBe('API is down')
      expect(erreur).toBeInstanceOf([TON_EXCEPTION_500])
    }
  })
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment