Evellyn Lima

Manipulando o tempo com Jest

February 05, 2020

As vezes nós precisamos testar um comportamento que depende de um timer (setTimeout, setInterval, clearTimeout, clearInterval), o problema é que esses métodos não são bons de serem testados porque dependem do tempo real para acontecerem, ainda bem que o Jest disponibiliza funções para controlar a passagem do tempo.

Eu vou mostrar dois exemplos de como utilizar os fake timers do jest, o primeiro utilizando setTimeout e o segundo utilizando setInterval.

Esse é o código que utiliza o método setTimeout:

export function Timeout({ time }) {
  const [showFirst, setShowFirst] = useState(true)

  useEffect(() => {
    setTimeout(() => {
      setShowFirst(false)
    }, time)
  }, [])

  return <>{showFirst ? <p>Primeiro item</p> : <p>Segundo item</p>}</>
}

Esse trecho de código mostra, em um primeiro momento, um parágrafo com o texto “Primeiro item” e após um período de tempo (passado por props) mostrará o texto “Segundo item”.

O teste desse código fica assim:

import React from "react"
import { act } from "react-dom/test-utils"
import { render, waitForElement } from "@testing-library/react"

import Timeout from "./"

/*
Para conseguir manipular o tempo dentro dos testes precisamos avisar 
ao Jest que vamos utilizar os fake timers
*/
jest.useFakeTimers()

test("Texto deve mudar após 5 segundos", async () => {
  // Renderizamos o componente passando o valor de 5 segundos por props
  const { getByText } = render(<Timeout time={5000} />)

  getByText("Primeiro item")

  act(() => {
    /*
    Com o método "advanceTimersByTime" o Jest avança todos timers 
    com o tempo (milisegundos) definido
    */
    jest.advanceTimersByTime(5000)
  })

  /*
  Como avançamos o timer em 5 segundos e o segundo item apareceria após 
  5 segundos aqui ele já está presente na tela
  */
  getByText("Segundo item")
})

O segundo exemplo a ser testado utiliza o método “setInterval”. Nós temos 3 items, a cada X tempo (passado por props) um item fica ativo, quando o último item é o ativo e é o momento de mudar de item, tornamos o primeiro item ativo novamente:

(Obs: o código poderia ser escrito de uma maneira melhor/diferente, mas eu preferi deixar ele dessa maneira porque acredito que fica mais simples de ser entendido.)

export function Timeout({ time }) {
  const [activeItem, setActiveItem] = useState(0)

  useEffect(() => {
    updateCurrent()
  }, [activeItem])

  const updateCurrent = () => {
    setInterval(() => {
      const c = activeItem === 2 ? 0 : activeItem + 1
      setActiveItem(c)
    }, time)
  }

  return (
    <>
      <div
        data-testid="item"
        className={activeItem === 0 ? "item active" : "item"}
      >
        item 1
      </div>
      <div
        data-testid="item"
        className={activeItem === 1 ? "item active" : "item"}
      >
        item 2
      </div>
      <div
        data-testid="item"
        className={activeItem === 2 ? "item active" : "item"}
      >
        item 3
      </div>
    </>
  )
}

O teste do código acima fica assim:

import React from "react"
import { act } from "react-dom/test-utils"
import { render } from "@testing-library/react"

import Timeout from "./"

// Avisamos ao Jest que vamos utilizar timers falsos
jest.useFakeTimers()

describe("teste time out", () => {
  it("deve marcar item como active corretamente", async () => {
    // Renderizamos o componente passando o valor de 3 segundos por props
    const { getAllByTestId } = render(<Timeout time={3000} />)

    // Pegamos todos os items com o ID "item"
    const items = getAllByTestId("item")

    // Validamos que os 3 items estão na tela
    expect(items).toHaveLength(3)

    // Inicia com o primeiro item ativo
    expect(items[0]).toHaveClass("active") // Item ativo
    expect(items[1]).not.toHaveClass("active")
    expect(items[2]).not.toHaveClass("active")

    act(() => {
      // Avança 3 segundos no tempo
      jest.advanceTimersByTime(3000)
    })

    /*
    Após avançar 3 segundos o método dentro do setInterval é chamado 
    novamente, o segundo item é o ativo agora
    */
    expect(items[0]).not.toHaveClass("active")
    expect(items[1]).toHaveClass("active") // Item ativo
    expect(items[2]).not.toHaveClass("active")

    act(() => {
      // Avança 3 segundos no tempo
      jest.advanceTimersByTime(3000)
    })

    /*
    O método dentro do setInterval é chamado novamente e o terceiro 
    item é o ativo agora
    */
    expect(items[0]).not.toHaveClass("active")
    expect(items[1]).not.toHaveClass("active")
    expect(items[2]).toHaveClass("active") // Item ativo

    act(() => {
      // Avança 3 segundos no tempo
      jest.advanceTimersByTime(3000)
    })

    /*
    O método dentro do setInterval é chamado novamente, agora o 
    primeiro item deve estar ativo novamente
    */
    expect(items[0]).toHaveClass("active")
    expect(items[1]).not.toHaveClass("active")
    expect(items[2]).not.toHaveClass("active")
  })
})

O Jest disponibiliza outros métodos para facilitar os testes que envolvem timers, você pode ler sobre eles aqui.


Eu sou a Evellyn Lima, desenvolvedora web [por acaso] desde 2014 e a favor de JS sem ponto e vírgula.
Twitter LinkedIn