27

I am trying to spy on useState React hook but i always get the test failed

This is my React component:

const Counter= () => {
    const[counter, setCounter] = useState(0);

    const handleClick=() => {
        setCounter(counter + 1);
    }

    return (
        <div>
            <h2>{counter}</h2>
            <button onClick={handleClick} id="button">increment</button>
        </div>
    )
}

counter.test.js:

it('increment counter correctlry', () => {
    let wrapper = shallow(<Counter/>);
    const setState = jest.fn();
    const useStateSpy = jest.spyOn(React, 'useState');

    useStateSpy.mockImplementation((init) => [init, setState]);
     const button = wrapper.find("button")
     button.simulate('click');
     expect(setState).toHaveBeenCalledWith(1);
})

Unfortunately this doesn't work and i get the test failed with that message:

expected 1
Number of calls: 0
diedu
  • 19,277
  • 4
  • 32
  • 49
Saher Elgendy
  • 1,519
  • 4
  • 16
  • 27
  • Does this answer your question? [How to mock the useState hook implementation so that it actually changes the state during testing](https://stackoverflow.com/questions/57692521/how-to-mock-the-usestate-hook-implementation-so-that-it-actually-changes-the-sta) – dwjohnston Oct 02 '20 at 01:28
  • I don't think so, my problem seems simpler – Saher Elgendy Oct 02 '20 at 01:41
  • 1
    I agree that your question is simpler. However, do just search 'mock usestate' in stackoverflow and see many other questions regarding this. Also the answer to your question in the linked question probably applies. – dwjohnston Oct 02 '20 at 01:43
  • I am new to testing and Jest, and i got this code already after some search, but unfortunately it doesn't work https://gist.github.com/agiveygives/721785723a23fe997ba2df21561f37e3 – Saher Elgendy Oct 02 '20 at 01:49
  • you should check the snapshot after the click event, then you can see the value of the counter – iamhuynq Oct 02 '20 at 02:00
  • 1
    well, there is a comment on the gist that says it works https://gist.github.com/agiveygives/721785723a23fe997ba2df21561f37e3#gistcomment-3465908 you just need to use `React.useState()` – diedu Oct 02 '20 at 02:02
  • @iamhuynq the value in the corresponding snapshot is still 0, this problem is so stubborn – Saher Elgendy Oct 02 '20 at 02:03
  • @diedu, please check my cod in the question, i am already using `useState` hook in my functional component – Saher Elgendy Oct 02 '20 at 02:04
  • 1
    the comment says to use `React.useState()` instead of importing `{ useState } from 'react'` – diedu Oct 02 '20 at 02:05
  • 1
    also, you're mocking useState, so you shouldn't expect it to actually update the state, you can only check it's been called – diedu Oct 02 '20 at 02:06
  • @diedu, i can't believe this works finally, but why i should use React.useState(), do you have an answer, thank you you can add your answer so i can accept if you want!! – Saher Elgendy Oct 02 '20 at 02:17
  • 1
    I honestly don't know, let me research why and I'll add the answer so people coming later can learn something new – diedu Oct 02 '20 at 02:24
  • And i will be here to accept, thank you you saved my day! – Saher Elgendy Oct 02 '20 at 02:27
  • 1
    There is legitimately no point to this test, you are gaining practically zero confidence that you application behaves correctly. I will create a codesanbox to explain how a component like this should be tested. One moment – Alex Mckay Oct 02 '20 at 02:44

7 Answers7

26

diedu's answer led me the right direction and I came up with this solution:

  1. Mock use state from react to return a jest.fn() as useState:
    1.1 Also import useState immediately after - that will now be e jest mock (returned from the jest.fn() call)

jest.mock('react', ()=>({
  ...jest.requireActual('react'),
  useState: jest.fn()
}))
import { useState } from 'react';
  1. Later on in the beforeEach, set it to the original useState, for all the cases where you need it to not be mocked

describe("Test", ()=>{
  beforeEach(()=>{
    useState.mockImplementation(jest.requireActual('react').useState);
    //other preperations
  })
  //tests
})
  1. In the test itself mock it as needed:

it("Actual test", ()=>{
  useState.mockImplementation(()=>["someMockedValue", someMockOrSpySetter])
})

Parting notes: While it might be conceptually somewhat wrong to get your hands dirty inside the "black box" one is unit testing, it is indeed super useful at times to do it.

Robotnik
  • 3,643
  • 3
  • 31
  • 49
doni
  • 291
  • 3
  • 9
23

You need to use React.useState instead of the single import useState.

I think is about how the code gets transpiled, as you can see in the babel repl the useState from the single import ends up being different from the one of the module import

_react.useState // useState
_react.default.useState // React.useState;

So you spy on _react.default.useState but your component uses _react.useState. It seems impossible to spyOn a single import since you need the function to belong to an object, here is a very extensive guide that explains the ways of mocking/spying modules https://github.com/HugoDF/mock-spy-module-import

And as @Alex Mackay mentioned, you probably want to change your mindset about testing react components, moving to react-testing-library is recommended, but if you really need to stick to enzyme you don't need to go that far as to mock react library itself

diedu
  • 19,277
  • 4
  • 32
  • 49
  • You can need to mock `useState` not to know whether it has been called but to prevent errors and warnings on console (like `wrap your component in act()`) and other issues when `useState` is called. So mocking it to only return dumb data under control is an efficient way of preventing these issues. – AxeEffect Dec 13 '20 at 12:27
  • Similar article https://www.linkedin.com/pulse/mocking-react-hooks-usestate-useeffect-leonard-lin – Michael Freidgeim Apr 10 '22 at 20:38
7

just you need to import React in your test file like:

import * as React from 'react';

after that you can use the mock function.

import * as React from 'react';

:
:
it('increment counter correctlry', () => {
    let wrapper = shallow(<Counter/>);
    const setState = jest.fn();
    const useStateSpy = jest.spyOn(React, 'useState');

    useStateSpy.mockImplementation((init) => [init, setState]);
     const button = wrapper.find("button")
     button.simulate('click');
     expect(setState).toHaveBeenCalledWith(1);
})
William Penagos
  • 141
  • 2
  • 3
  • Thanks for the post, William. How does this work with multiple useStates in a component? – jsibs Aug 02 '21 at 22:30
  • better is don't use multiple useStates, you should use one with a middle function like: ``` setStateFn = (key, value) => setState((oldState) => ({ ...oldState, [key]: value })); ``` – William Penagos Aug 03 '21 at 23:12
  • 1
    can you please update you're answer so that it is inclusive of the case? I think it would help others. Thank you for the time. – jsibs Aug 03 '21 at 23:14
  • 1
    If you have multiple states, you can do something like: jest.spyOn(React, 'useState') .mockImplementation(() => [1, jest.fn()]) .mockImplementation(() => ['mySecondState', jest.fn()]); Respecting the same order of the component. Not ideal, but it works. – Carolyne Stopa Aug 30 '22 at 19:41
  • Why `import React from 'react';` is not working & `import * as React from 'react';` works – ASharma7 Apr 04 '23 at 15:06
6

Annoyingly Codesandbox is currently having trouble with its testing module so I can't post a working example but I will try to explain why mocking useState is generally a bad thing to do.

The user doesn't care if useState has been called, they care about when I click increment the count should increase by one therefore that is what you should be testing for.

// App
import React, { useState } from "react";
export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
    </div>
  );
}
// Tests
import React from "react";
import App from "./App";
import { screen, render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

describe("App should", () => {
  it('increment count value when "Increment" btn clicked', () => {
    // Render the App
    render(<App />);
    // Get the count in the same way the user would, by looking for 'Count'
    let count = screen.getByText(/count:/);
    // As long as the h1 element contains a '0' this test will pass
    expect(count).toContain(0);
    // Once again get the button in the same the user would, by the 'Increment'
    const button = screen.getByText(/increment/);
    // Simulate the click event
    userEvent.click(button);
    // Refetch the count
    count = screen.getByText(/count:/);
    // The 'Count' should no longer contain a '0'
    expect(count).not.toContain(0);
    // The 'Count' should contain a '1'
    expect(count).toContain(1);
  });
  // And so on...
  it('reset count value when "Reset" btn is clicked', () => {});
  it('decrement count value when "Decrement" btn is clicked', () => {});
});

Definitely check out @testing-library if you are interested in this style of testing. I switched from enzyme about 2 years ago and haven't touched it since.

Alex Mckay
  • 3,463
  • 16
  • 27
  • Thank you, that is a good answer, but i tried the same solution using enzyme but also failed, simulating the button click then logging the counter also printed 0, i really will migrate to react testing library, thank you – Saher Elgendy Oct 02 '20 at 03:47
  • Why when run this code i get the test fail with that message expected 1 received

    counter 1

    – Saher Elgendy Oct 06 '20 at 03:43
  • 1
    Add this [library](https://github.com/testing-library/jest-dom) and use the matcher `.toHaveTextContent`. See [here specifically](https://github.com/testing-library/jest-dom#tohavetextcontent) – Alex Mckay Oct 08 '20 at 05:53
  • 1
    Alternatively, use `count.innerHTML` or `count.textContent` – Alex Mckay Oct 08 '20 at 05:54
  • I am trying below, but so far it ahs not worked.. const setAnotherSuburbDetails = jest.fn(); const setUserSelectedAddressIndex = jest.fn(); React.useState .mockImplementation(() => ['', setAnotherSuburbDetails]) .mockImplementation(() => [0, setUserSelectedAddressIndex]); wrapper.dive().find('RadioGroup').simulate('change', 'Another suburb'); expect(setAnotherSuburbDetails).toHaveBeenCalledWith(1); Not sure , where I am going wrong getting error as _react.default.useState.mockImplementation is not a function – sk215 Mar 02 '23 at 02:54
3

you should use React.useState() instead useState(), But there are other ways... in React you can set useState without React with this config

// setupTests.js
    const { configure } = require('enzyme')
    const Adapter = require('@wojtekmaj/enzyme-adapter-react-17')
    const { createSerializer } = require('enzyme-to-json')

    configure({ adapter: new Adapter() });
    expect.addSnapshotSerializer(createSerializer({
        ignoreDefaultProps: true,
        mode: 'deep',
        noKey: true,
    }));
import React, { useState } from "react";

    const Home = () => {

        const [count, setCount] = useState(0);

        return (
            <section>

                <h3>{count}</h3>
                <span>
                    <button id="count-up" type="button" onClick={() => setCount(count + 1)}>Count Up</button>
                    <button id="count-down" type="button" onClick={() => setCount(count - 1)}>Count Down</button>
                    <button id="zero-count" type="button" onClick={() => setCount(0)}>Zero</button>
                </span>
            </section>
        );

    }

    export default Home;

// index.test.js

    import { mount } from 'enzyme';
    import Home from '../';
    import React, { useState as useStateMock } from 'react';


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

    describe('<Home />', () => {
        let wrapper;

        const setState = jest.fn();

        beforeEach(() => {
            useStateMock.mockImplementation(init => [init, setState]);
            wrapper = mount(<Home />);
        });

        afterEach(() => {
            jest.clearAllMocks();
        });

        describe('Count Up', () => {
            it('calls setCount with count + 1', () => {
                wrapper.find('#count-up').simulate('click');
                expect(setState).toHaveBeenCalledWith(1);
            });
        });

        describe('Count Down', () => {
            it('calls setCount with count - 1', () => {
                wrapper.find('#count-down').props().onClick();
                expect(setState).toHaveBeenCalledWith(-1);
            });
        });

        describe('Zero', () => {
            it('calls setCount with 0', () => {
                wrapper.find('#zero-count').props().onClick();
                expect(setState).toHaveBeenCalledWith(0);
            });
        });
    });
1

You don't need to use `Import * as React from 'react', since it causes issues if you don't use React.useState in your code.

It worked for me to just do the following:

Import React from 'react';


describe('the test suite', () => {
  afterEach(() => restoreAllMocks());
  
  it('the test', () => {
    const setter = jest.fn();
    
    jest.spyOn(React, 'useState').mockImplementation(() => [true, setter]);
    
    // do something
    
    expect(setter).toHaveBeenCalled();
  })
})
Spine1
  • 99
  • 1
  • 2
0

I believe in this example you don't need to mock the useState because it is into in the same component, different if you send counter and setCounter in a other child component. I resolved your example of this mode:

import {useState} from "react";
import {render, screen} from '@testing-library/react'
import userEvent from "@testing-library/user-event";

const Counter= () => {
    const[counter, setCounter] = useState(0);

    const handleClick=() => {
        setCounter(counter + 1);
    }

    return (
        <div>
            <h2>{counter}</h2>
            <button onClick={handleClick} id="button">increment</button>
        </div>
    )
}

describe("Counter", () => {
    it("should render component correctly", ()=>{
        render(<Counter/>)
        expect(screen).not.toBeNull()
    })
    it("Should do click in button and increment counter", () => {
        render(<Counter/>)
        expect(screen.getByText("0")).toBeInTheDocument()
        const button = screen.getByRole("button")
        userEvent.click(button)
        expect(screen.getByText("1")).toBeInTheDocument()
    })
})