Testing a component that uses useEffect using Enzyme shallow and not mount - reactjs

// MyComponent.jsx
const MyComponent = (props) => {
const { fetchSomeData } = props;
useEffect(()=> {
fetchSomeData();
}, []);
return (
// Some other components here
)
};
// MyComponent.react.test.jsx
...
describe('MyComponent', () => {
test('useEffect', () => {
const props = {
fetchSomeData: jest.fn(),
};
const wrapper = shallow(<MyComponent {...props} />);
// THIS DOES NOT WORK, HOW CAN I FIX IT?
expect(props.fetchSomeData).toHaveBeenCalled();
});
});
When running the tests I get:
expect(jest.fn()).toHaveBeenCalled()
Expected mock function to have been called, but it was not called.
The expect fails because shallow does not call useEffect. I cannot use mount because of other issues, need to find a way to make it work using shallow.

useEffect is not supported by Enzyme's shallow rendering. It is on the roadmap (see column 'v16.8+: Hooks') to be fixed for the next version of Enzyme, as mentioned by ljharb
What you're asking is not possible with the current setup. However, a lot of people are struggling with this.
I've solved / worked around this by:
not using shallow rendering from Enzyme anymore
use the React Testing Library instead of Enzyme
mocking out modules via Jest
Here's a summary on how to mock modules, based on Mock Modules from the React docs.
contact.js
import React from "react";
import Map from "./map";
function Contact(props) {
return (
<div>
<p>
Contact us via foo#bar.com
</p>
<Map center={props.center} />
</div>
);
}
contact.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Contact from "./contact";
import MockedMap from "./map";
jest.mock("./map", () => {
return function DummyMap(props) {
return (
<p>A dummy map.</p>
);
};
});
it("should render contact information", () => {
const center = { lat: 0, long: 0 };
act(() => {
render(
<Contact
name="Joni Baez"
email="test#example.com"
site="http://test.com"
center={center}
/>,
container
);
});
});
Useful resources:
React Testing Library docs - mocking
React docs - Mocking Modules

Here's a solution from a colleague of mine at CarbonFive:
https://blog.carbonfive.com/2019/08/05/shallow-testing-hooks-with-enzyme/
TL;DR: jest.spyOn(React, 'useEffect').mockImplementation(f => f())

shallow doesn't run effect hooks in React by default (it works in mount though) but you could use jest-react-hooks-shallow to enable the useEffect and useLayoutEffect hooks while shallow mounting in enzyme.
Then testing is pretty straightforward and even your test specs will pass.
Here is a link to a article where testing the use-effect hook has been clearly tackled with shallow mounting in enzyme
https://medium.com/geekculture/testing-useeffect-and-redux-hooks-using-enzyme-4539ae3cb545
So basically with jest-react-hooks-shallow for a component like
const ComponentWithHooks = () => {
const [text, setText] = useState<>();
const [buttonClicked, setButtonClicked] = useState<boolean>(false);
useEffect(() => setText(
`Button clicked: ${buttonClicked.toString()}`),
[buttonClicked]
);
return (
<div>
<div>{text}</div>
<button onClick={() => setButtonClicked(true)}>Click me</button>
</div>
);
};
you'd write tests like
test('Renders default message and updates it on clicking a button', () => {
const component = shallow(<App />);
expect(component.text()).toContain('Button clicked: false');
component.find('button').simulate('click');
expect(component.text()).toContain('Button clicked: true');
});

I'm following this advice and using mount() instead of shallow(). Obviously, that comes with a performance penalty, so mocking of children is advised.

Related

How to properly mock useRef hook with react-testing-library and jest

I am having some problems to properly mock useRef hook in am project I am developing. I am using React with Typescript in my project, and using react testing library and jest to unit test my components.
After going throughout the documentation and a few blog posts, I am still unsure why I am unable to make it work correctly as it seems a fairly simple case. I have isolated the problem in a CodeSandbox.
This would be the main component of my example to replicate the problem. I simply have a button that executes a method implemented in the referenced component and the referenced component itself to which I pass the ref.
import { useRef } from "react";
import ReferencedComponent, { References } from "./ReferencedComponent";
export default function UseRefTest() {
const refApiCallUiFeedback = useRef<References>(null);
return (
<>
<button onClick={() => refApiCallUiFeedback.current?.ReferencedMethod()}>
BUTTON
</button>
<ReferencedComponent ref={refApiCallUiFeedback} />
</>
);
}
This is the actual "ReferencedComponent". It is a dummy component containing a method which is exposed via "useImperativeHandle"
import React, { Ref, useImperativeHandle } from "react";
export interface References {
ReferencedMethod: () => void;
}
function ReferencedComponentWrapped(empty: {}, ref: Ref<References>) {
function ReferencedMethod() {
console.log("Calling real referenced method");
}
useImperativeHandle(ref, () => ({
ReferencedMethod
}));
return (
<>
<div>Referenced Component</div>
</>
);
}
const ReferencedComponent = React.forwardRef(ReferencedComponentWrapped);
export default ReferencedComponent;
Finally this it the unit test in which I simply fire the click event and expect the method to be called.
import React, { RefObject } from "react";
import { render, fireEvent, screen } from "#testing-library/react";
import UseRefTest from "../UseRefTest";
import { References } from "../ReferencedComponent";
describe("Testing if useRef Mocking", () => {
beforeEach(() => {});
afterEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();
jest.clearAllMocks();
});
test("is working as expected", async () => {
//ARRANGE
const mockReferencedMethod = jest.fn(() =>
console.log("Calling mocked referenced method")
);
const refs: RefObject<References> = {
current: { ReferencedMethod: mockReferencedMethod }
};
jest.spyOn(React, "useRef").mockReturnValue(refs);
render(<UseRefTest />);
//ACT
fireEvent.click(screen.getByRole("button"));
//ASSERT
expect(mockReferencedMethod).toBeCalled();
});
});
You can find here the full working example -> CodeSandBox

React useEffect Hook fires prematurely in React Testing Library and Enzyme tests

I am writing unit tests in React Testing Library and Enzyme for a React component that has a useEffect hook. When I run the tests I get an error from the useEffect hook saying that the accessTheRef function is undefined. If I move the accessTheRef function above the useEffect hook I then get an error that the ref is undefined.
Component:
import React, { useEffect } from 'react';
import { fireEvent, render } from '#testing-library/react';
const RefTest = ({theFunc}) => {
const ref = React.useRef(null);
useEffect(() => {
accessTheRef()
}, [])
const accessTheRef = () => {
const newValue = ref.current?.value
theFunc(newValue)
};
return (
<div>
<input ref={ref} value='defined' readOnly={true}/>
<button type="button" onClick={(e) => accessTheRef()}>
Select text
</button>
</div>
);
};
Test:
describe('<RefTest>', () => {
it('has a null ref in testing environments in the initial call of useEffect', () => {
const theFunc = jest.fn()
const rendered = render(<RefTest theFunc={theFunc} />);
fireEvent(
rendered.getByText('Select text'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
})
)
expect(theFunc.mock.calls[0][0]).toBe(undefined) // what on earth why
expect(theFunc.mock.calls[1][0]).toBe('defined')
})
});
The code works without any errors in the browser. The error occurs when I run the test with both the React Testing Library tests and the Enzyme tests. It seems like useEffect is not properly deferring and is firing before layout and paint have finished.
Why am I running into these errors in my testing environment? How can I fix this issue in my tests?

How do you use Enzyme to check for changes to a React Hooks component after onClick?

I am trying to write a simple integration test in my 100% React Hooks (React v16.12) project with Enzyme (v3.10), Jest (v24.0) and TypeScript where if I click a button component in my App container, another component displaying a counter will go up by one. The current value of the counter is stored in the state of the App container (see snippets below).
Basically, I mount the App component to render its children, then try to simulate a click on the button with Enzyme and check the props of the counter display component to see if its value has gone up. But nothing happens. Not only does the onClick handler not get called but I don't seem to be able to retrieve the value prop I pass to the PaperResource component. So basically I can't test the counter display changes when I click on the button in my Enzyme integration test! The test asserts that the value prop goes from 0 to 1, but this assertion fails without an error per seenter code here. Is this because Enzyme support for Hooks is still not there yet or am I doing something daft here? When I run the app on my browser, everything works as expected.
Here's my integration test
import React from 'react';
import App from './App';
import { mount, ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import MakePaperButton from './components/MakePaperButton';
import PaperResource from './components/PaperResource';
describe('App', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
act(() => {
wrapper = mount(<App />);
});
});
describe('when make paper button is clicked', () => {
beforeEach(() => {
act(() => {
wrapper.find('.make-paper__button').simulate('click');
});
});
it('should increase paper resource', () => {
expect(wrapper.find('.resources__paper').prop('value')).toEqual(1);
});
});
});
And here is my React code
import React, { useState } from 'react';
import './App.scss';
import MakePaperButton from './components/MakePaperButton';
import PaperResource from './components/PaperResource';
const App: React.FC = () => {
const [ resources, setResources ] = useState({
paper: 0,
});
const handleMakePaperButtonClick = () => {
setResources({
...resources,
paper: resources.paper + 1,
});
};
return (
<div className="App">
<MakePaperButton onClick={handleMakePaperButtonClick} />
<div className="resources">
<PaperResource value={resources.paper} />
</div>
</div>
);
}
export default App;
My components are very simple
// PaperResource.tsx
import React from 'react';
export default (props: { value: number }) => (
<div className="resources__paper">
<span>Paper: {props.value}</span>
</div>
);
// MakePaperButton.tsx
import React from 'react';
export default (props: { onClick: () => void }) => (
<div className="make-paper__button">
<button onClick={props.onClick}>Make Paper</button>
</div>
);
The only solution I've found so far is wrapping the expect statement in a setTimeout().
it('should increase paper resource', () => {
setTimeout(() => {
expect(wrapper.find('.resources__paper').prop('value')).toEqual(1);
}, 0);
});

Testing a component that uses React.useEffect using enzyme

Let's say I have this functional React component.
const SomeComponent = ({ onShow }) => {
React.useEffect(onShow, []);
return <div />;
};
Now I want to unit test that SomeComponent calls onShow the first time it is rendered.
Is that possible using enzyme?
I have tried implementing two very similar tests, the difference being that the first is using enzyme, the other one using react-testing-library.
The test that uses react-testing-library passes, but the enzyme test fails, even though they test the same code.
Example:
import * as reactTestingLibrary from "react-testing-library";
import * as React from "react";
import * as enzyme from "enzyme";
const SomeComponent = ({ onShow }) => {
React.useEffect(onShow, []);
return <div />;
};
describe("Testing useEffect with enzyme", () => {
it("calls the side effect", async () => {
const aFn = jest.fn();
enzyme.mount(<SomeComponent onShow={aFn} />);
expect(aFn).toHaveBeenCalledTimes(1); // this test fails
});
});
describe("Testing useEffect with react-testing-library", () => {
it("calls the side effect", async () => {
const aFn = jest.fn();
reactTestingLibrary.render(<SomeComponent onShow={aFn} />);
expect(aFn).toHaveBeenCalledTimes(1); // this test passes
});
});
Is there a way to make enzyme execute the hook and pass the test?
Yes, it is possible to test components that uses hooks, using enzyme.
Upgrading enzyme-adapter-react-16 to latest version (1.12.1) fixed the problem.
i just upgraded my enzyme-adapter-react-16 to v1.12.1, that sorted it out for me.

How test a React Loadable component

I have this component:
import React from 'react';
const UploadAsync = Loadable({
loader: () => import('components/Upload'),
loading: () => <LoadingComponent full />
});
const Component = () => {
return <UploadAsync />
}
export default Component
And the test:
import React from 'react';
import Component from './index';
describe('Component', () => {
it('should render correctly with upload component', () => {
const tree = create(<Component />).toJSON();
expect(tree).toMatchSnapshot();
});
});
How I can see the Upload component and not the Loading component in the snapshot?
exports[`Content should render correctly with form component 1`] = `
<div>
<Loading
full={true}
/>
</div>
`;
So far I have tried setTimeOut and Promises.
Use Loadable.preloadAll() before the tests then you can access the Component you need.
Docs
Simple example:
all imports here
Loadable.preloadAll()
describe('TestName', () => {
//tests
})
I haven't been able to figure this out either, but here are a couple of workarounds that, alone or together, may work reasonably well:
Snapshot test the components that are passed to Loadable
In your case, the test would look something like this:
import React from 'react';
import Component from './components/Upload';
describe('Component', () => {
it('should render correctly with upload component', () => {
const tree = create(<Component />).toJSON();
expect(tree).toMatchSnapshot();
});
});
You could also test <LoadingComponent full /> in a similar fashion. No, this doesn't assure you that the Loadable component is working, but you may find it satisfactory to assume that the react-loadable library is well tested and will work as long as you pass to it your own, properly tested components.
End-to-end browser testing
Using a framework such as Selenium or TestCafe you can write tests that run against your site as it runs in a real browser.
It seems like there is no proper solution but if your test is actually rendering the component in browser inside iframe then you can get your react component by Jquery contentDocument
$('#component-iframe')[0].contentDocument
you can find for some specific element in your component by class or id using
$('#component-iframe')[0].contentDocument.getElementById('componentID')
I have a solution, which I found accidentally, but I don't understand how it works (maybe if we could figure out, we could solve this problem). For now, it's good for a workaround.
If you call mount once outside the test, the dynamic module will load magically:
function mountApp() {
return mount(
<ApolloProvider client={apolloClient}>
<MemoryRouter initialEntries={['/']}>
<App />
</MemoryRouter>
</ApolloProvider>
);
}
mountApp();
// Tests
test('react-loadable module loads', () => {
const wrapper = mountApp();
console.log(wrapper.debug());
});
Here, the App, which contains react-loadable modules loads correctly with all its content available. When I remove the first mountApp, it doesn't work anymore (it loads only the loading string).
Edit:
Actually it works inside the test too, but this way you only need to do this once for every test to work.
I was trying to test a component which had Loadable-components inside of it, and run into a similar problem. I managed to mock those components and that made the parent-component mount (enzyme) as I wanted it to.
I'm leaving out the mocked store and props, as they aren't relevant to the solution
const component = () => {
return mount(
<Provider store={store}>
<DoubleMatrix {...props} />
</Provider>
)
}
// These are the Loadable-components, import { Grid, GridColumn } from 'App/Components/Tables/Grid' in the parent component which I am testing
jest.mock('App/Components/Tables/Grid', () => ({
Grid: () => <div />, // eslint-disable-line
GridColumn: () => <div />, // eslint-disable-line
}))
it('renders', () => {
const wrapper = component()
expect(wrapper).toMatchSnapshot()
})
Here is how you test the loaded component:
import {LoadableFoo} from './loadable-foo';
import {Foo} from './Foo';
it('should load the Foo component', async () => {
// usual shallow render
const component = shallow(<LoadableFoo/>);
// prerender the Loadable component
const loadedComponent = await component.preload();
expect(loadedComponent.Foo).toEqual(Foo);
});

Resources