57

When I tested class component with enzyme I could do wrapper.setState({}) to set state. How can I do the same now, when I am testing function component with useState() hook?

For example in my component I have:

const [mode, setMode] = useState("my value");

And I want to change mode inside my test

isherwood
  • 58,414
  • 16
  • 114
  • 157
Anna
  • 2,911
  • 6
  • 29
  • 42

3 Answers3

36

When using state from hooks, your test must ignore implementation details like state in order to properly test it. You can still make sure the component passes the correct state into its children.

You can find a great example in this blog post written by Kent C. Dodds.

Here's an excerpt from it with a code example.

Test that relies on state implementation details -

test('setOpenIndex sets the open index state properly', () => {
  const wrapper = mount(<Accordion items={[]} />)
  expect(wrapper.state('openIndex')).toBe(0)
  wrapper.instance().setOpenIndex(1)
  expect(wrapper.state('openIndex')).toBe(1)
})

Test that does not rely on state implementation details -

test('counter increments the count', () => {
  const {container} = render(<Counter />)
  const button = container.firstChild
  expect(button.textContent).toBe('0')
  fireEvent.click(button)
  expect(button.textContent).toBe('1')
})
Moti Azu
  • 5,392
  • 1
  • 23
  • 32
  • 23
    Oh.. so we cannot test state when we use hooks – Anna Mar 26 '19 at 07:59
  • 3
    Not directly as of now. It's hard for me to imagine how that would be possible because of the syntax of hooks relying on call order and not naming. I look at it like testing classes with no access to privates - it's nice to have the access but it usually means you could have written it better :) – Moti Azu Mar 26 '19 at 08:13
  • 2
    Hm, I guess it is true that tests actually should not rely on state changing, but should test the appearance – Anna Mar 26 '19 at 14:20
  • 1
    Appearance is hard to test, but if you have a state it's usually passed on to a child component as a prop, that you can make sure has gotten in the way you expect it. Or even an API call mock with the state. – Moti Azu Mar 26 '19 at 14:21
  • 23
    So I can say that the famous Kent C. Dodds is really wrong. This is what happens when rely a lot on the words of programmers that only knows how to Tweet. I work for IBM and we were obligated to test the hooks. Obviously we MUST to test functionality that goes along hooks, we can not ignore the facts that they exist and are critical. I followed some guidance from this post https://blog.carbonfive.com/2019/08/05/shallow-testing-hooks-with-enzyme/ and also I will be posting and answer here soon to let you know how to REALLY test hooks and it is not like Kent said. He is just wrong. – Reacting Mar 06 '20 at 02:13
  • 2
    The second test is as good as an E2E test, its not an UNIT test where the consumer is the developer and not the browser – Cristian E. Feb 09 '21 at 16:40
11

This is the way that I found to do it, I'm not saying this is right or wrong. In my case, a block of code was dependent on state being set to a particular value. I will keep my opinions about testing in React to myself.

In your test file: Adjust your import for the react library

import * as React from 'react'

Then in your test spy on useState and mock its implementation

const stateSetter = jest.fn()
jest
.spyOn(React, 'useState')
//Simulate that mode state value was set to 'new mode value'
.mockImplementation(stateValue => [stateValue='new mode value', stateSetter])

Please be aware that mocking useState this will way apply to all instances where useState is called for your test, so if you have more than one state value that you are looking at, they will all be set to 'new mode value'. Someone else may be able to help you sort that out. Hope it helps.

Doug Moses
  • 176
  • 1
  • 6
5

At top of test file, can be defined first as:

  import { useState } from 'react';

  jest.mock('react', () => ({
    ...jest.requireActual('react'),
    useState: jest.fn()
  }));

  const useStateMock: jest.Mock<typeof useState> = useState as never;

After that at each test can be used with different value which is wanted to be tested:

  const setValue = jest.fn();
  useStateMock
    .mockImplementation(() => ['value', setValue]);

Furkan Uyar
  • 376
  • 4
  • 5