How to set initial state for useState Hook in jest and enzyme?
Asked Answered
G

11

44

Currently Im using functional component with react hooks. But I'm unable to test the useState hook completely. Consider a scenario like, in useEffect hook I'm doing an API call and setting value in the useState. For jest/enzyme I have mocked data to test but I'm unable to set initial state value for useState in jest.

const [state, setState] = useState([]);

I want to set initial state as array of object in jest. I could not find any setState function as similar like class component.

Guiana answered 14/7, 2019 at 8:29 Comment(3)
show code pleaseGrill
Can you share your full component code and the error?Bingaman
Don't. test what the component looks like to the user. not what it does internally.Hornstone
N
36

You can mock React.useState to return a different initial state in your tests:

// Cache original functionality
const realUseState = React.useState

// Stub the initial state
const stubInitialState = ['stub data']

// Mock useState before rendering your component
jest
  .spyOn(React, 'useState')
  .mockImplementationOnce(() => realUseState(stubInitialState))

Reference: https://dev.to/theactualgivens/testing-react-hook-state-changes-2oga

Newark answered 6/8, 2019 at 21:0 Comment(11)
What if I had multiple useState statements into the component?Damara
Mock multiple useState with a succession of mockImplementationOnce(). See jestjs.io/docs/en/mock-functions.html#mock-return-valuesNewark
What if you don't know which order the useState's are called? I have a complicated app with many useStates. I probably shouldn't be testing implementation at this point.Dicentra
how to get the return state value, is it possible? Such below const [formSend, setArrayValues] = React.useState(); Coyle
@BenButterworth You found an answer to that? I have the same situation as you - I can't mock useState because I'm using it in my component; and I can't use mockImplementationOnce because I'm not sure exactly how many times useState is going to be called.Chaperon
Sorry @Chaperon I haven't used React much since thenDicentra
You should know how many useState calls you have, hook calls are expected to be unconditional and always in the same order, after all that is how React uses it, it does not know which is which, it relies on the order of the callsCrissum
I disagree. The method of using many mockImplementationOnce implicitly depends on the order of the useState calls, however the order of the useState calls themselves does not matter. This would make your tests flakey, for example if you decide to reorganize your useState calls to alphabetical order (or whatever reason)Madid
This working for me thanks you. I write the code from scratch to make sure its workingKellikellia
I am trying like below const setUserSelectedAddressIndex = jest.fn(); const setOriginalAddressList = jest.fn(); jest.spyOn(React, 'useState') .mockReturnValueOnce([0, setUserSelectedAddressIndex]) .mockReturnValueOnce([mockList, setOriginalAddressList]) but states are not getting setEssene
I am so confused, trying to get back into jest... but its like I have to mock everything, which is ridiculous... I have mock fetch, each function that uses fetch, every state, it's insane, this can't be right... I mean, really?Vocation
S
29

First, you cannot use destructuring in your component. For example, you cannot use:

import React, { useState } from 'react';
const [myState, setMyState] = useState();

Instead, you have to use:

import React from 'react'
const [myState, setMyState] = React.useState();

Then in your test.js file:

test('useState mock', () => {
   const myInitialState = 'My Initial State'

   React.useState = jest.fn().mockReturnValue([myInitialState, {}])
   
   const wrapper = shallow(<MyComponent />)

   // initial state is set and you can now test your component 
}

If you use useState hook multiple times in your component:

// in MyComponent.js

import React from 'react'

const [myFirstState, setMyFirstState] = React.useState();
const [mySecondState, setMySecondState] = React.useState();

// in MyComponent.test.js

test('useState mock', () => {
   const initialStateForFirstUseStateCall = 'My First Initial State'
   const initialStateForSecondUseStateCall = 'My Second Initial State'

   React.useState = jest.fn()
     .mockReturnValueOnce([initialStateForFirstUseStateCall, {}])
     .mockReturnValueOnce([initialStateForSecondUseStateCall, {}])
   
   const wrapper = shallow(<MyComponent />)

   // initial states are set and you can now test your component 
}
// actually testing of many `useEffect` calls sequentially as shown
// above makes your test fragile. I would recommend to use 
// `useReducer` instead.
Shirleeshirleen answered 17/12, 2020 at 4:45 Comment(7)
Could you elaborate on why we can't use destructuring? Because as far as I am aware, it's legal code for React components.Chaperon
What does this achieve? useState is an implementation detail that test shouldn't know or care about. Set up your component's state indirectly as a black box using props and by interacting with the UI as the user would. The fact that you're recommending avoiding destructuring, a commonplace React idiom, purely to assist injecting mocks is a huge red flag that the test knows too much about the component's internals.Polygamous
I also don’t understand why we should use React.useState, could somebody explain?Saxony
@Saxony You need to replace the useState with a jest.fn(). This solution involves overwriting the reference: React.useState = jest.fn(). If its destructured, then you are calling a standalone function in your component and cannot replace it with the jest function.Scourings
The description is clearKellikellia
This is getting to ludacris, I just want to test a full featured big old react component... I don't want to have to remock the hole thing, that's insaine. There has to be a better way... I mean, maybe if tests were writen before code this makes sense... but our application is 8 years old, 100k lines, and zero tests... can't even get the first one going, have to mock 90% of the component.. fetch, components that use it, every state, react, probably every lib in use... common man, what am I missing?Vocation
Downvoting this because it makes no sense saying we can't use destructuring in this scenario, or at all.Fireside
V
13

If I recall correctly, you should try to avoid mocking out the built-in hooks like useState and useEffect. If it is difficult to trigger the state change using enzyme's invoke(), then that may be an indicator that your component would benefit from being broken up.

Vote answered 6/8, 2019 at 21:7 Comment(6)
Exactly. Instead of mocking state, trigger the state changes by interacting with the component through the UI and/or mocking external API responses. Hooks are implementation details.Polygamous
@Polygamous So no need to test useEffect() logic?Breast
@Polygamous I think I get what you're saying. But what if I am not able to trigger the useEffect logic by mocking user interaction? For example some logic inside useEffect that runs only once on component mountBreast
Sure, useEffect runs when the component mounts, but there's almost always some sort of observable effect visible to the user sooner or later. Test that. If the useEffect fires a fetch call to an API to get a list of the user's posts, I'd intercept that and mock the repsonse, then assert that the mocked posts show up in the UI. None of this involves having any idea that the component is functional rather than class or uses useEffect. There's no need to mock React.Polygamous
@Polygamous sure, thanks for this answer. So from coverage perspective which we configure in jest to have a minimum coverage like 70% of all lines, we can't do anything about it in terms of covering useEffect lines right?Breast
Testing Implementation Details says it better than I can -- avoiding testing implementation details is about validating functionality that the user sees, not myopic line-by-line behavior.Polygamous
B
11

SOLUTION WITH DE-STRUCTURING

You don't need to use React.useState - you can still destructure in your component.

But you need to write your tests in accordance to the order in which your useState calls are made. For example, if you want to mock two useState calls, make sure they're the first two useState calls in your component.

In your component:

import React, { useState } from 'react';

const [firstOne, setFirstOne] = useState('');
const [secondOne, setSecondOne] = useState('');

In your test:

import React from 'react';

jest
.spyOn(React, 'useState')
.mockImplementationOnce(() => [firstInitialState, () => null])
.mockImplementationOnce(() => [secondInitialState, () => null])
.mockImplementation((x) => [x, () => null]); // ensures that the rest are unaffected
Brower answered 1/4, 2022 at 20:19 Comment(1)
what about if we only need to mock the last one? Would we need to mock all of them?Jordan
C
7
//Component    
const MyComponent = ({ someColl, someId }) => {
     const [myState, setMyState] = useState(null);

     useEffect(() => {loop every time group is set
         if (groupId) {
             const runEffect = async () => {
                  const data = someColl.find(s => s.id = someId);
                  setMyState(data);
             };
             runEffect();
         }
     }, [someId, someColl]);

     return (<div>{myState.name}</div>);
};

// Test
// Mock
const mockSetState = jest.fn();
jest.mock('react', () => ({
    ...jest.requireActual('react'),
    useState: initial => [initial, mockSetState]
}));
const coll = [{id: 1, name:'Test'}, {id: 2, name:'Test2'}];

it('renders correctly with groupId', () => {
    const wrapper = shallow(
        <MyComponent comeId={1} someColl={coll} />
    );
    setTimeout(() => {
        expect(wrapper).toMatchSnapshot();
        expect(mockSetState).toHaveBeenCalledWith({ id: 1, name: 'Test' });
    }, 100);
});
Crackleware answered 1/10, 2020 at 16:28 Comment(0)
L
6
  • Below function will return state
const setHookState = (newState) =>
  jest.fn().mockImplementation(() => [
    newState,
    () => {},
  ]);

Add below to use react

const reactMock = require('react');

In your code, you must use React.useState() to this work, else it won't work

const [arrayValues, setArrayValues] = React.useState();`
const [isFetching, setFetching] = React.useState();

Then in your test add following, mock state values

reactMock.useState = setHookState({
  arrayValues: [],
  isFetching: false,
});

Inspiration: Goto

Lacylad answered 27/2, 2020 at 15:55 Comment(6)
how do you pass the reactMock to the mounted component ?Noami
You don't have to, because you are mocking the useState function within react hooksLacylad
Works for me, thanks. I also use import React, { useState } from 'react';, so you can freely use spreaded version of useState with this approach.Pamela
In your code, you must use React.useState() to this work, else it won't work. How we can use useState() directly without React?Guipure
I tried it with only useState(),but it didn't work for meLacylad
I tried this solution but it seems that it didn't overwrite my initial value for my state. Can you please check my branch here? github.com/apappas1129/firestore-chat/tree/jestingSax
C
1

I have spent a lot of time but found good solution for testing multiple useState in my app.

export const setHookTestState = (newState: any) => {
  const setStateMockFn = () => {};
  return Object.keys(newState).reduce((acc, val) => {
    acc = acc?.mockImplementationOnce(() => [newState[val], setStateMockFn]);
    return acc;
  }, jest.fn());
};

where newState is object with state fields in my component;

for example:

React.useState = setHookTestState({
      dataFilter: { startDate: '', endDate: '', today: true },
      usersStatisticData: [],
    });
Conflux answered 14/1, 2022 at 10:23 Comment(0)
A
1

I used for multiple useState() Jest mocks the following setup in the component file

const [isLoading, setLoading] = React.useState(false);
const [isError, setError] = React.useState(false);

Please note the useState mock will just work with React.useState() derivation.

..and in the test.js

describe('User interactions at error state changes', () => {

const setStateMock = jest.fn();
beforeEach(() => {
    const useStateMock = (useState) => [useState, setStateMock];
    React.useState.mockImplementation(useStateMock) 
    jest.spyOn(React, 'useState')
    .mockImplementationOnce(() => [false, () => null]) // this is first useState in the component
    .mockImplementationOnce(() => [true, () => null]) // this is second useState in the component
});

it('Verify on list the state error is visible', async () => {
    render(<TodoList />);
    ....
Absurd answered 23/1, 2023 at 13:48 Comment(0)
I
0

NOT CHANGING TO React.useState

This approach worked for me:

//import useState with alias just to know is a mock
import React, { useState as useStateMock } from 'react'

//preseve react as it actually is but useState
jest.mock('react', () => ({
  ...jest.requireActual('react'),
  useState: jest.fn(),
}))

describe('SearchBar', () => {
  const realUseState: any = useStateMock //create a ref copy (just for TS so it prevents errors)

  const setState = jest.fn() //this is optional, you can place jest.fn directly
  beforeEach(() => {
    realUseState.mockImplementation((init) => [init, setState]) //important, let u change the value of useState hook
  })

  it('it should execute setGuestPickerFocused with true given that dates are entered', async () => {
    jest
      .spyOn(React, 'useState')
      .mockImplementationOnce(() => ['', () => null]) //place the values in the order of your useStates
      .mockImplementationOnce(() => ['20220821', () => null]) //...
      .mockImplementationOnce(() => ['20220827', () => null]) //...

    jest.spyOn(uiState, 'setGuestPickerFocused').mockReturnValue('')
    getRenderedComponent()
    expect(uiState.setGuestPickerFocused).toHaveBeenCalledWith(true)
  })
})

My component

const MyComp: React.FC<MyCompProps> = ({
  a,
  b,
  c,
}) => {
  const [searchQuery, setSearchQuery] = useState('') // my first value
  const [startDate, setStartDate] = useState('') // my second value
  const [endDate, setEndDate] = useState('') // my third value

  useEffect(() => {
    console.log(searchQuery, startDate, endDate) // just to verifiy
  }, [])

Hope this helps!

Iconostasis answered 29/7, 2022 at 19:2 Comment(0)
Z
0

heres how you can do it easy without Enzyme. And you can even do this if you useContext.

MyComponent.js

const [comments, setComments] = useState();

MyComponent.test.js

const comments = [{id:1, title: "first comment", body: "bla bla"}]
jest.spyOn(React, 'useState').mockReturnValueOnce([comments, jest.fn()]);
const { debug } = render(<MyComponent />);
debug();

Last two lines of code is to kinda see how the DOM would look like, to see if how your comments state looks like when rendered.

Zebrass answered 28/4, 2023 at 11:41 Comment(0)
C
0

My goal was to set initial values for useState. but the solutions I found seemed odd* to me, and when I tried to apply them, I was unable to set the state after mocking it. So I decided to use the initial optional props of the component instead.

export default function Chat({ initialLoading = false, initialDataSource = []}:ChatProps) {

  const [loading, setLoading] = useState<boolean>(initialLoading);
  const [dataSource, setDataSource] = useState<TableDataType[]>(initialDataSource);
  it('shows a table correctly', () => {
    const mockData = mockDataSource;
    const firstSupplier = mockData[0].supplier_company;

    render(<Chat initialDataSource={mockData} />);

    expect(screen.getByText(firstSupplier)).toBeInTheDocument();
  });

*What if I change the order of the code? Do I have to reorganize for the test again?

my answer does not respond the question but if someone is facing a same problem as me you can use this as an alternative approach.

Cyte answered 22/1 at 3:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.