Alexandre do Sim
Alexandre do Sim

Reputation: 13

React component isn't re rendering the fetch data in unit test

I'm trying to test a context with dummy child component.

Dummy component

function DefaultMockComponent() {

  const values = useContext(CurrencyContext)
  const valuesItems = Object.keys(values).map(value => {
    if (typeof values[value] === 'function') {
      return <div key={value} data-testid={value}>{value + '()'}</div>
    } else {
      return <div key={value} data-testid={value}>{values[value]}</div>
    }
  })

  return(
    <>
      {valuesItems}
    </>
  )
}

Context unit test

  // This test don't re render with the new data from fetch
  it('should pass the correct values', async () => {

    // This mock FAILS (see edit 2)
    fetch
      .mockResponse(req => {
        if (/.*\/ticker\/.*/.test(req.url)) {
          return new Promise(() => ({ body: JSON.stringify(tickerResponse) }))

        } else if (/.*\/day-summary\/.*/.test(req.url)) {
          return new Promise(() => ({ body: JSON.stringify(summaryResponse) }))

        }
      })

    render(
      <CurrencyProvider>
        <DefaultMockComponent />
      </CurrencyProvider>
    )

    await waitFor(() => expect(screen.getByTestId('currency').textContent).toBe("btc"))
    await waitFor(() => expect(screen.getByTestId('volBRL').textContent).toBe("41198719.10154287"))
    await waitFor(() => expect(screen.getByTestId('closing').textContent).toBe("326900.00666999"))
    await waitFor(() => expect(screen.getByTestId('sell').textContent).toBe("360000.00000000"))
    await waitFor(() => expect(screen.getByTestId('buy').textContent).toBe("359999.99006000"))
    await waitFor(() => expect(screen.getByTestId('last').textContent).toBe("359999.99006000"))
    await waitFor(() => expect(screen.getByTestId('vol').textContent).toBe("259.88030295"))
    await waitFor(() => expect(screen.getByTestId('low').textContent).toBe("353684.14000000"))
    await waitFor(() => expect(screen.getByTestId('high').textContent).toBe("380000.00000000"))
  });

Here fetch is mocked. I'm reading the MockComponent information and comparing it with the response object information. My problem is that the MockComponent renders only with the default context state values, as you can see:

Rendered MockComponent

    <body>
      <div>
        <div
          data-testid="high"
        >
          0
        </div>
        <div
          data-testid="low"
        >
          0
        </div>
        <div
          data-testid="vol"
        >
          0
        </div>
        <div
          data-testid="last"
        >
          0
        </div>
        <div
          data-testid="buy"
        >
          0
        </div>
        <div
          data-testid="sell"
        >
          0
        </div>
        <div
          data-testid="closing"
        >
          0
        </div>
        <div
          data-testid="volBRL"
        >
          0
        </div>
        <div
          data-testid="currency"
        >
          btc
        </div>
        <div
          data-testid="setCurrency"
        >
          setCurrency()
        </div>
      </div>
    </body>

Which is strange since my Cypress E2E test works fine. How can I make the DefaultMockComponent to render the data returned from fetch?

EDIT 1

CurrencyProvider of CurrencyContext

export default function CurrencyProvider({children}) {
  const [currency, setCurrency] = useState('btc')
  const [high, setHigh] = useState('0')
  const [low, setLow] = useState('0')
  const [vol, setVol] = useState('0')
  const [last, setLast] = useState('0')
  const [buy, setBuy] = useState('0')
  const [sell, setSell] = useState('0')
  const [closing, setClosing] = useState('0')
  const [volBRL, setVolBRL] = useState('0')

  useEffect(update, [currency])

  function update() {
    const ticker = `https://www.mercadobitcoin.net/api/${currency}/ticker/`

    fetch(ticker)
      .then(response => response.json())
      .then(data => {
        setHigh(data.ticker.high)
        setLow(data.ticker.low)
        setVol(data.ticker.vol)
        setLast(data.ticker.last)
        setBuy(data.ticker.buy)
        setSell(data.ticker.sell)
      })

    const date = new Date()
    date.setDate(date.getDate() - 1)

    const year = date.getFullYear()
    const month = date.getMonth() + 1
    const day = date.getDate()

    const summary = `https://www.mercadobitcoin.net/api/${currency}/day-summary/${year}/${month}/${day}/`

    fetch(summary)
      .then(response => response.json())
      .then(data => {
        setClosing(data.closing)
        setVolBRL(data.volume)
      })
  }

  return(
    <CurrencyContext.Provider value={{
      high,
      low,
      vol,
      last,
      buy,
      sell,
      closing,
      volBRL,
      currency,
      setCurrency
    }}>
      {children}
    </CurrencyContext.Provider>
  )
}

EDIT 2

Thanks to lissettdm. Now the fetch function isn't failing, but the values still not being updated plus I'm getting "An update to CurrencyProvider inside a test was not wrapped in act(...)" warning. Following the mock code that works:

Mock without jest-fetch-mock (as lissettdm answer)

    const fetchSpy = jest.spyOn(window, "fetch").mockImplementation((req) =>
      Promise.resolve({
        json: () => {
          if (/.*\/ticker\/.*/.test(req)) {
            return tickerResponse
          } else if (/.*\/day-summary\/.*/.test(req)) {
            return summaryResponse
          }
        },
      })
    );

Mock with jest-fetch-mock

fetch
      .mockResponse(req => {
        if (/.*\/ticker\/.*/.test(req.url)) {
          return Promise.resolve({ body: JSON.stringify(tickerResponse) })

        } else if (/.*\/day-summary\/.*/.test(req.url)) {
          return Promise.resolve({ body: JSON.stringify(summaryResponse) })

        }
      })

Upvotes: 1

Views: 763

Answers (1)

lissettdm
lissettdm

Reputation: 13080

Problem:

You see only the default context values because the fetch function is failing. The first then expect that the response object contains json function and that part is missing.

Solution:

You need to add json function to your mock implementation :

const fetchSpy = jest.spyOn(window, "fetch").mockImplementationOnce((req) =>
  Promise.resolve({
    json: () => {
     if (/.*\/ticker\/.*/.test(req.url)) {
        return new Promise(() => ({ body: JSON.stringify(tickerResponse) }))
     } else if (/.*\/day-summary\/.*/.test(req.url)) {
        return new Promise(() => ({ body: JSON.stringify(summaryResponse) }))
     }
   },
 }));
 //--> check if fetch was called
 expect(fetchSpy).toHaveBeenCalled();

About this warning:

An update to CurrencyProvider inside a test was not wrapped in act(...)" warning

Probably, your testing is ending without the state be flushed. I think you should use findBy methods which are a combination of getBy queries and waitFor

 const currency = await screen.getByTestId('currency');
 expect(currency.textContent).toBe("btc");

Upvotes: 1

Related Questions