enzyme: Testing Hooks with shallow: Invariant Violation

Current behavior

When testing component which contains newly released React Hooks using shallow, it crashes: Invariant Violation: Hooks can only be called inside the body of a function component.

Everything works fine at run time or when testing with render:

My test component:

import * as React from 'react';

function Test() {
    const [myState, setMyState] = React.useState('Initial state');

    const changeState = () => setMyState('State updated');

    return (
        <div>
            {myState}
            <button onClick={changeState}>Change</button>
        </div>
    );
}

export default Test;

My test file:

import { shallow } from 'enzyme';
import * as React from 'react';
import Test from './Test';

it('renders without crashing', () => {
    const comp = shallow(<Test />);

    expect(comp.find('Test')).toMatchSnapshot();
});

Error stack trace:

Invariant Violation: Hooks can only be called inside the body of a function component.

    at invariant (node_modules/react/cjs/react.development.js:125:15)
    at resolveDispatcher (node_modules/react/cjs/react.development.js:1450:28)
    at Object.useState (node_modules/react/cjs/react.development.js:1473:20)
    at Test (src/Test.tsx:4:11)
    at node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:440:38
    at ReactShallowRenderer.render (node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:412:39)
    at node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:444:37
    at withSetStateAllowed (node_modules/enzyme-adapter-utils/build/Utils.js:137:16)
    at Object.render (node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:443:70)
    at new ShallowWrapper (node_modules/enzyme/build/ShallowWrapper.js:206:22)
    at Object.shallow (node_modules/enzyme/build/shallow.js:21:10)
    at Object.<anonymous> (src/Test.test.tsx:6:18)
        at new Promise (<anonymous>)
    at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
    at process._tickCallback (internal/process/next_tick.js:68:7)

Expected behavior

Tests should run

Your environment

Fresh create-react-app-typescript install with react 16.7.0-alpha-0

API

  • shallow
  • mount
  • render

Version

library version
enzyme 3.8.0
react 16.7.0-alpha.0
react-dom 16.7.0-alpha.0
react-test-renderer
adapter (below)

Adapter

  • enzyme-adapter-react-16
  • enzyme-adapter-react-16.3
  • enzyme-adapter-react-16.2
  • enzyme-adapter-react-16.1
  • enzyme-adapter-react-15
  • enzyme-adapter-react-15.4
  • enzyme-adapter-react-14
  • enzyme-adapter-react-13
  • enzyme-adapter-react-helper
  • others ( )

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 39
  • Comments: 79 (31 by maintainers)

Commits related to this issue

Most upvoted comments

I had the same issue and the reason was multiple versions of react-test-renderer

$ yarn why react-test-renderer yarn why v1.16.0 [1/4] Why do we have the module “react-test-renderer”…? [2/4] Initialising dependency graph… [3/4] Finding dependency… [4/4] Calculating file sizes… => Found “react-test-renderer@16.8.6” info Has been hoisted to “react-test-renderer” info This module exists because it’s specified in “dependencies”. => Found “enzyme-adapter-react-16#react-test-renderer@16.2.0” info This module exists because “enzyme-adapter-react-16” depends on it. Done in 1.74s.

For some reason, enzyme-adapter-react-16 had an old react-test-renderer@16.2.0 as its own dependency. I had to remove and add them back:

yarn remove enzyme enzyme-adapter-react-16 react-test-renderer yarn add enzyme enzyme-adapter-react-16 react-test-renderer

Hooks are not yet in a non-alpha version, and are definitely not yet supported in enzyme.

I’d suggest waiting to use them until they’re not experimental.

@icyJoseph I managed to get this working by upgrading everything around react and testing:

"enzyme": "3.8.0"
"enzyme-adapter-react-16": "1.9.1",
"react": "16.8.1",
"react-dom": "16.8.1",
"react-test-renderer": "16.8.1",

@bdwain Hi 😃 You wrote, that you’ve been able to run a simple test with useState, I’ve alse written one simple case and unfortunately it did not work. Maybe you know what is wrong here or maybe it is some React bugs - I am not sure, and I don’t want to create new issue if it is only my mystake

   import React, { useState } from 'react';

    const TestComponent = () => {
        const [myState, setMyState] = useState('1234');

        return (
            <>
                <input onChange={({ target: { value } }) => setMyState(value)} />
                <p>{myState}</p>
            </>
        );
    };
    export { TestComponent };
    describe('Test Copoment', () => {
        it('should change myState value on input change', () => {
            // Given
            const testComponent = shallow(<TestComponent />);
            const { onChange } = testComponent.find('input').props();

            // Then 
            expect(testComponent.find('p')).toHaveText('1234');

            // When
            act(() => {
                onChange({ target: { value: '8658' } });
                components.update();
            });

            // Then 
            expect(testComponent.find('p')).toHaveText('8658');
        });
    });

// Text is still 1234 (initial state)

@icyJoseph

From what I can tell, the problem with the shallow test in https://github.com/airbnb/enzyme/issues/1938#issuecomment-467027468 is unrelated to the title of this issue (“Hooks can only be called inside the body of a function component”). This is why this issue is confusing to me.

It would be great if there were separate issues for separate problems. This particular issue (“Hooks can only be called inside the body of a function component”) has already been fixed in React.

The issue you’re seeing (state update doesn’t trigger re-render) is a bug in React shallow renderer. We’re tracking it in https://github.com/facebook/react/issues/14840.

As for Enzyme’s setState helper — indeed, I don’t see it working with components using Hooks. Components using Hooks may have arbitrary number of internal states. The intended testing strategy for them is that you trigger the actual interactions that call those state setters rather than call them directly.

This works for now. You need to add react-test-renderer if it’s not already a dependency.


const ComponentWithHooks = () => {
  const [count, setCount] = useState(0);
  return <p onClick={() => setCount(count + 1)}>Count is {count}</p>;
};
describe('ComponentWithHooks', () => {
  it('explodes', () => {
    const wrapper = shallow(<ComponentWithHooks />); // Invariant Violation: Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)
  });

  it("doesn't explode with a workaround", () => {
    const renderer = new ShallowRenderer();
    renderer.render(<ComponentWithHooks />);
    const output = renderer.getRenderOutput();
    const wrapper = shallow(<div>{output}</div>); // Have to wrap it, otherwise you get: TypeError: ShallowWrapper can only wrap valid elements
    // ...
  });
});

I’m cutting 16.8.5 now. It includes the shallow rendering fixes.

I verified that Enzyme suite passes, and that applying https://github.com/airbnb/enzyme/pull/2014 on top of 16.8.5 will also fix all cases @wodenx collected in https://github.com/airbnb/enzyme/issues/1938#issuecomment-461276294 (thanks for that!)

If something’s still broken please file an issue. Thanks.

@icyJoseph Here’s a working example with mount. Didn’t manage to get it working with shallowactually

The component Test.tsx

import * as React from 'react';

function Test() {
    const [myState, setMyState] = React.useState('initial_state');

    const changeState = () => setMyState('updated_state');

    return (
        <div>
            <div className="stateContent">{myState}</div>
            <button onClick={changeState}>Change</button>
        </div>
    );
}

export default Test;

Test file Test.test.tsx

import { mount } from 'enzyme';
import * as React from 'react';
import Test from './Test';

it('renders without crashing', () => {
    const comp = mount(<Test />);

    comp.find('button').simulate('click');

    expect(comp.find('div.stateContent').text()).toBe('updated_state');
});

Hope this helps!

I know it’s been said a number of times above, but the main part of the suggested solutions for me was to force yarn to force the resolution of react-test-renderer to match my react version:

So I basically ended up with:

..snip..
"dependencies": {
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "enzyme": "^3.9.0",
    "enzyme-adapter-react-16": "^1.12.1",
},
"resolutions": {
    "react-test-renderer": "^16.8.6"
  }
}

Updating everything solved the problem for me: "devDependencies": { "enzyme": "^3.9.0", "enzyme-adapter-react-16": "^1.4.0", "eslint": "^5.16.0", "react": "^16.8.6", "react-dom": "^16.8.6", "react-test-renderer": "^16.6.2" }

Hooks have been released and this still seems to be an issue with enzyme’s shallow renderer. Is there any sort of ETA on getting an update to enzyme to support hooks?

An open issue in react facebook/react/#14091

Merged at facebook/react#14567

@koszatnik12 I have the same, with enzyme. I updated to lates 16.8.5 but simple unit test with useState does not work for me.

Upgrading react-test-render as @joepuzzo said fixed my problem.

@ljharb Hooks are labeled “upcoming” now 🤔.

v3.10.0 has now been released.

You can follow the progress here (useEffect with shallow): https://github.com/airbnb/enzyme/issues/2086

any update?

useState works with shallow already, thanks! but useEffect still behaves differently with mount and shallow:

import React, { useEffect, useState } from 'react';
import { mount, shallow } from 'enzyme';

export const MyComponent = () => {
  const [value, setValue] = useState('');
  useEffect(() => {
    setValue('effected');
  }, []);
  return (
    <div>
      <button onClick={() => setValue('clicked')} />
      <span>{value}</span>
    </div>
  );
};

describe('With mount()', () => {
  const wrapper1 = mount(<MyComponent />);
  wrapper1.find('button').simulate('click');
  it('changes value', () => expect(wrapper1.find('span').text()).toContain('clicked'));

  const wrapper2 = mount(<MyComponent />);
  it('calls useEffect', () => expect(wrapper2.find('span').text()).toContain('effected'));
});

describe('With shallow()', () => {
  const wrapper1 = shallow(<MyComponent />);
  wrapper1.find('button').simulate('click');
  it('changes value', () => expect(wrapper1.find('span').text()).toContain('clicked'));

  const wrapper2 = shallow(<MyComponent />);
  it('calls useEffect', () => expect(wrapper2.find('span').text()).toContain('effected'));
});
With mount()
    ✓ changes value (4ms)
    ✓ calls useEffect (1ms)
  With shallow()
    ✓ changes value (1ms)
    ✕ calls useEffect (8ms)
---
Expected string:
      ""
    To contain value:
      "effected"

btw: useLayoutEffect have the same issue

I get ShallowWrapper::setState() can only be called on class components if i try to setState on a functional component that uses the useState hook.

Can somebody please create an example project demonstrating what doesn’t work? On the React side, we expect shallow rendering a component with Hooks to work just fine.

@ljharb lol i was just checking that - i just upgraded from 16.8.0 to 16.8.1 and have the same results.

@chenesan that would be great. The biggest priority is getting as many tests as possible, and mirroring those as much as possible between shallow and mount.

Fair enough, but they’re still both not yet a thing in a real version, and enzyme doesn’t yet have support for them.

This solution worked for us, but we also realised we did not need react-test-renderer at all, so we removed it from package.json completely

yarn remove enzyme enzyme-adapter-react-16 react-test-renderer
yarn add enzyme enzyme-adapter-react-16

Test cases with Hooks still works

updating enzyme-adapter-react-16 to version 1.15.1 fixed the problem for me

I created a react issue for this

@gaearon Yea I was planning on filing an issue with React. Are you saying file the issue with enzyme instead because the react shallow renderer should not run the effects? I am worried that this can’t be solved with just enzyme, which is why I was planning on filing the issue with react.

It sounds like it would be difficult for enzyme to keep track of the effects without digging into internals of react because the effects are usually just anonymous functions inside a component (whereas in classes it can call instance.componentDidUpdate() or something). Similar to how keeping track of state from useState is difficult since there’s not one single state object.

This is by design because code in them is usually not resilient to shallow rendering. If you want to test effects, you probably want to use mount (and possibly mock out some child components that you don’t want in your test).

That made it sound like it was a choice by the react team to not run the effects in the shallow renderer. That’s why I was thinking it would be nice if it exposed some option to enable running effects on an opt-in basis, where you are aware of the potential side effects. If I’m wrong and it’s a big effort to do it from react then that might change things, but it seemed like the best approach to me.

@gaearon I have this sandbox.

There you can find a Counter inside Hooks.

With these two tests.

The enzyme test fails, the one using your suggested approach passes.

I am having a possibly related issue with 16.8. I don’t get the Invariant Violation, but some hooks which persist data across renders (useRef and useState) are not behaving as expected during shallow render, but work fine during mount.

Versions:

├─ enzyme-adapter-react-16@1.9.1
├─ enzyme@3.8.0
├─ jest@23.6.0
├─ react-dom@16.8.1
└─ react@16.8.1

Tests:

import * as React from 'react';
import { shallow, mount } from 'enzyme';

class ClassInstance extends React.Component {
  constructor(props) {
    super(props);
    this.id = Math.random().toString();
  }

  render() {
    return <div>{this.id}</div>;
  }
}

const SFCRef = () => {
  const id = React.useRef(Math.random().toString());
  return <div>{id.current}</div>;
};

const SFCState = () => {
  const [id] = React.useState(Math.random().toString());
  return <div>{id}</div>;
};

test('1 class instance property persists with shallow', () => {
  const wrapper = shallow(<ClassInstance foo="a" />);
  const id = wrapper.text();
  wrapper.setProps({ foo: 'b' });
  const id1 = wrapper.text();
  expect(id).toBe(id1);
});

test('2 class instance property persists with mount', () => {
  const wrapper = mount(<ClassInstance foo="a" />);
  const id = wrapper.find('div').text();
  wrapper.setProps({ foo: 'b' });
  const id1 = wrapper.find('div').text();
  expect(id).toBe(id1);
});

test('3 SFC ref persists with mount', () => {
  const wrapper = mount(<SFCRef foo="a" />);
  const id = wrapper.find('div').text();
  wrapper.setProps({ foo: 'b' });
  const id1 = wrapper.find('div').text();
  expect(id).toBe(id1);
});

test('4 SFC ref persists with shallow', () => {
  const wrapper = shallow(<SFCRef foo="a" />);
  const id = wrapper.text();
  wrapper.setProps({ foo: 'b' });
  const id1 = wrapper.text();
  expect(id).toBe(id1);
});

test('5 SFC state persists with mount', () => {
  const wrapper = mount(<SFCState foo="a" />);
  const id = wrapper.find('div').text();
  wrapper.setProps({ foo: 'b' });
  const id1 = wrapper.find('div').text();
  expect(id).toBe(id1);
});

test('6 SFC state persists with shallow', () => {
  const wrapper = shallow(<SFCState foo="a" />);
  const id = wrapper.text();
  wrapper.setProps({ foo: 'b' });
  const id1 = wrapper.text();
  expect(id).toBe(id1);
});

// Verify that an id which *should not* persist across renders in fact does not.
const SFC = () => {
  const id = Math.random().toString();
  return <div>{id}</div>;
}

test('7 SFC alone does not persist with mount', () => {
  const wrapper = mount(<SFC foo="a" />);
  const id = wrapper.find('div').text();
  wrapper.setProps({ foo: 'b' });
  const id1 = wrapper.find('div').text();
  expect(id).not.toBe(id1);
});

test('8 SFC alone does not persist with shallow', () => {
  const wrapper = shallow(<SFC foo="a" />);
  const id = wrapper.text();
  wrapper.setProps({ foo: 'b' });
  const id1 = wrapper.text();
  expect(id).not.toBe(id1);
});

Results:

  ✓ 1 class instance property persists with shallow (8ms)
  ✓ 2 class instance property persists with mount (31ms)
  ✓ 3 SFC ref persists with mount (3ms)
  ✕ 4 SFC ref persists with shallow (11ms)
  ✓ 5 SFC state persists with mount (2ms)
  ✕ 6 SFC state persists with shallow (2ms)
  ✓ 7 SFC alone does not persist with mount (3ms)
  ✓ 8 SFC alone does not persist with shallow

Reopening, since https://github.com/facebook/react/pull/14679 is now merged and v16.8 seems imminent.

Also tracked in #1553.

An open issue in react facebook/react/#14091

This are my dependencies, and I still get this error (hook should not be called outside of balblabla)

    "react": "^16.14.0",
    "react-dom": "^16.13.1",
    "react-test-renderer": "16.14.0",
    "enzyme": "^3.11.0",
    "enzyme-adapter-react-16": "^1.15.2"

Does it matter that between my test file and the inner tested component there is a class based one? SOmething like this

mount( <Provider><TestedClassComponent/></Provider>

Where TestedClassComponent is a class component that internally uses a functional component that depends on the provider? (which is also functional)

@icyJoseph I managed to get this working by upgrading everything around react and testing:

"enzyme": "3.8.0"
"enzyme-adapter-react-16": "1.9.1",
"react": "16.8.1",
"react-dom": "16.8.1",
"react-test-renderer": "16.8.1",

That worked for me too, thanks !

I was getting intermittent testing failures even after https://github.com/airbnb/enzyme/issues/1938#issuecomment-500637733 . I was able to get my specs passing consistently by doing the following:

yarn upgrade react react-dom --exact
yarn remove enzyme enzyme-adapter-react-16 react-test-renderer
yarn add enzyme enzyme-adapter-react-16 react-test-renderer

Then using Enzyme’s mount (instead of shallow) in my failing specs (found after reading this post).

@alexanderkjeldaas the next one.

but useEffect still behaves differently with mount and shallow

useEffect still doesn’t work with shallow

It is expected that shallow won’t call useEffectjust like shallow rendering never invoked componentDidMount or componentDidUpdate.

This is by design because code in them is usually not resilient to shallow rendering. If you want to test effects, you probably want to use mount (and possibly mock out some child components that you don’t want in your test).


currentTab event after simulate click does not rerender, but props I pass to onChange come correctly

File a new issue in React repo if you think it’s a React bug. A repository with reproducing case would be very helpful.

As I already wrote earlier, state not updating is a separate bug in React. It has already been fixed in master, but the fix is not released yet. This bug has nothing to do with Enzyme. You’ll see the fix in the next React release (either 16.8.5 or 16.9.0).

@ljharb isnt that more of an alternative rather than a fix?

so it looks like my issue goes away when I upgrade react-test-renderer to the latest version. After reading more into these issues i found this https://github.com/facebook/react/pull/14567 and realized my issue was simply because i was relying on react-test-renderer@16.4.2. That all being said I do agree that the title of this issue is a little confusing because it seems like the case initially described in the first comment works just fine now.

If it’s still an issue with every React package (including the test renderer) at 16.8, then a PR with tests would be quite welcome.

I don’t know if there’s any ETA for supporting hooks but I’ll start to work on this in recent days 😃