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

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

Related

FE: Unit testing window.location.href changing on button click (test fails)

I am trying to test when the window.location.href changes after a button is clicked using react-testing-library. I have seen examples online where you manually update window.location.href inside of a test case as so window.location.href = 'www.randomurl.com' and then follow it with expect(window.location.href).toEqual(www.randomurl.com). While this indeed will pass, I want to avoid this as I'd rather simulate the user actions instead of injecting the new value into the test. If I do that, even if I remove my button click (which is what will actually trigger the function call) the expect will still pass because I have anyway manually updated the window.location.href in my test
What I've opted for is having goToThisPage func (which will redirect the user) to be placed outside of my functional component. I then mock goToThisPage in my test file and in my test case check whether it has been called. I do know that the goToThisPage is being triggered because I included a console.log and when I run my tests I see it in my terminal. Nonetheless, the test still fails. I have been playing around with both spyOn and jest.doMock/mock with no luck
component.js
import React from 'react'
import { ChildComponent } from './childcomponent';
export const goToThisPage = () => {
const url = '/url'
window.location.href = url;
console.log('reached');
};
export const Component = () => {
return (<ChildComponent goToThisPage={ goToThisPage }/>)
}
export default Component;
Test file:
import * as Component from './component'
import userEvent from '#testing-library/user-event';
jest.doMock('./component', () => ({
goToThisPage: jest.fn(),
}));
describe('goToThisPage', () => {
test('should call goToThisPage when button is clicked', async () => {
const goToThisPageSpy = jest.spyOn(Component, 'goToThisPage');
const { container, getByTestId } = render(<Component.Component />);
userEvent.click(screen.getByTestId('goToThisPage')); // this is successfully triggered (test id exists in child component)
expect(goToThisPageSpy).toHaveBeenCalled();
// expect(Component.goToThisPage()).toHaveBeenCalled(); this will fail and say that the value must be a spy or mock so I opted for using spy above
});
});
Note: when I try to just do jest.mock I got this error Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
When testing out with jest.doMock the error disappeared but the actual test fails.
I am open to hear more refined ideas of solving my issue if someone believes this solution could be improved. Thanks in advance
Edit:
This is another approach I have tried out
import { Component, goToThisPage } from './component'
import userEvent from '#testing-library/user-event';
describe('goToThisPage', () => {
test('should call goToThisPage when button is clicked', async () => {
const goToThisPageSpy = jest.spyOn(Component, 'goToThisPage');
// I am not certain what I'd put as the first value in the spy. Because `goToThisPage` is an external func of <Component/> & not part of the component
const { container, getByTestId } = render(<Component />);
userEvent.click(screen.getByTestId('goToThisPage'));
expect(goToThisPageSpy).toHaveBeenCalled();
});
});
Save yourself the headache and split the goToThisPage function into its own file. You seem to be mocking the goToThisPage function fine but when the Component is rendered with react testing library it doesn't seem render with the mocked function but defaults to what the function would normally do. This easiest way would be just to mock the function from its own file. If you truly want to keep the function in the same file you will need to make some adjustments, see (example #2) but I do not recommend this path.
See below for examples
Example 1: (Recommended) Split function into it's own file
Component.spec.jsx
import React from "react";
import Component from "./Component";
import { render, screen } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import * as goToThisPage from "./goToThisPage";
jest.mock('./goToThisPage');
describe("goToThisPage", () => {
test("should call goToThisPage when button is clicked", async () => {
const goToThisPageSpy = jest.spyOn(goToThisPage, 'default').mockImplementation(() => console.log('hi'));
render(<Component />);
userEvent.click(screen.getByTestId("goToThisPage"));
expect(goToThisPageSpy).toHaveBeenCalled();
});
});
goToThisPage.js
export const goToThisPage = () => {
const url = "/url";
window.location.href = url;
};
export default goToThisPage;
Component.jsx
import React from "react";
import ChildComponent from "./ChildComponent";
import goToThisPage from "./goToThisPage";
export const Component = () => {
return <ChildComponent goToThisPage={goToThisPage} />
};
export default Component;
Example 2: (Not Recommend for React components!)
We can also get it working by calling the goToThisPage function via exports. This ensures the component is rendered with our spyOn and mockImplementation. To get this working for both browser and jest you need to ensure we run the original function if it's on browser. We can do this by creating a proxy function that determines which function to return based on a ENV that jest defines when it runs.
Component.jsx
import React from "react";
import ChildComponent from "./ChildComponent";
export const goToThisPage = () => {
const url = "/url";
window.location.href = url;
};
// jest worker id, if defined means that jest is running
const isRunningJest = !!process.env.JEST_WORKER_ID;
// proxies the function, if jest is running we return the function
// via exports, else return original function. This is because
// you cannot invoke exports functions in browser!
const proxyFunctionCaller = (fn) => isRunningJest ? exports[fn.name] : fn;
export const Component = () => {
return <ChildComponent goToThisPage={proxyFunctionCaller(goToThisPage)} />
};
export default Component;
Component.spec.jsx
import React from "react";
import { render, screen } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
describe("goToThisPage", () => {
test("should call goToThisPage when button is clicked", async () => {
const Component = require('./Component');
const goToThisPageSpy = jest.spyOn(Component, 'goToThisPage').mockImplementation(() => console.log('hi'));
render(<Component.default />);
userEvent.click(screen.getByTestId("goToThisPage"));
expect(goToThisPageSpy).toHaveBeenCalled();
});
});
You can move the function proxy to it's own file but you need to pass exports into the proxy function as exports is scoped to it's own file.
Example code
// component.js
import React from "react";
import ChildComponent from "./ChildComponent";
import proxyFunctionCaller from "./utils/proxy-function-caller";
export const goToThisPage = () => {
const url = "/url";
window.location.href = url;
};
export const Component = () => {
return <ChildComponent goToThisPage={proxyFunctionCaller(typeof exports !== 'undefined' ? exports : undefined, goToThisPage)} />
};
export default Component;
// utils/proxy-function-caller.js
// jest worker id, if defined means that jest is running
const isRunningJest = !!process.env.JEST_WORKER_ID;
// proxies the function, if jest is running we return the function
// via exports, else return original function. This is because
// you cannot invoke exports functions in browser!
const proxyFunctionCaller = (exports, fn) => isRunningJest ? exports[fn.name] : fn;
export default proxyFunctionCaller;
There are other ways to do this but I would follow the first solution as you should be splitting utility functions into it's own files anyway. Goodluck.
Example 3 for #VinceN
You can mock a function that lives in the same file using the below example files.
SomeComponent.tsx
import * as React from 'react';
const someFunction = () => 'hello world';
const SomeComponent = () => {
return (
<div data-testid="innards">
{someFunction()}
</div>
)
}
export default SomeComponent;
SomeComponent.spec.tsx
import SomeComponent from './SomeComponent';
import { render, screen } from "#testing-library/react";
jest.mock('./SomeComponent', () => ({
__esModule: true,
...jest.requireActual('./SomeComponent'),
someFunction: jest.fn().mockReturnValue('mocked!')
}));
describe('<SomeComponent />', () => {
it('renders', () => {
render(<SomeComponent />);
const el = screen.getByTestId('innards');
expect(el.textContent).toEqual('mocked!');
});
});
You exporting both functions and then defining a default export of the Component itself is what's causing the problem (which is mixing up default and named exports).
Remove export default Component; and change the top import in your test file to import {Component, goToThisPage} from './component'. That said I'm not sure you even need to export goToThisPage (for the Jest test at least).

Problems testing a Redux + React app with enzyme:

I have this component
import React, { useEffect } from 'react';
import './App.css';
import { connect } from 'react-redux';
import { CircularProgress } from '#material-ui/core';
import { loadPhones } from './redux/actions/actions.js';
import TablePhones from './Table.js';
const mapStateToProps = (state) => state;
function mapDispatchToProps(dispatch) {
return {
loadPhones: () => {
dispatch(loadPhones());
},
};
}
export function App(props) {
useEffect(() => {
props.loadPhones();
}, []);
if (props.phones.data) {
return (
<div className="App">
<div className="introductoryNav">Phones</div>
<TablePhones phones={props.phones.data} />
</div>
);
}
return (
<div className="gridLoadingContainer">
<CircularProgress color="secondary" iconStyle="width: 1000, height:1000" />
<p className="loadingText1">Loading...</p>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
For whom ive written
import React from 'react';
import { render } from '#testing-library/react';
import { Provider } from "react-redux";
import App from './App';
import { shallow, mount } from "enzyme";
import configureMockStore from "redux-mock-store";
const mockStore = configureMockStore();
const store = mockStore({});
describe('App comp testing', () => {
it("should render without throwing an error", () => {
const app = mount(
<Provider store={store}>
<App />
</Provider>
).dive()
expect(app.find('.introductoryNav').text()).toContain("Phones");
});
})
But that test keeps failing
ypeError: Cannot read property 'data' of undefined
I also tried importing App as {App} instead and using shallow testing, but no luck. It gives the same erros, so im left without access to the context, and I cant keep doing my tests
How can I solve this?
You could use the non-default export of your component here and shallow render test if you pass your component the props and don't try to mock the store (if I recall correctly).
I was thinking something like this might work, tesing the "pure" non-store connected version of the component. This seems to be a popular answer for this question as this was asked (in a different way) before here:
import React from 'react';
import { App } from './App';
import { shallow } from "enzyme";
// useful function that is reusable for desturcturing the returned
// wrapper and object of props inside of beforeAll etc...
const setupFunc = overrideProps => {
const props = {
phones: {
...phones, // replace with a mock example of a render of props.phones
data: {
...phoneData // replace with a mock example of a render of props.phones.data
},
},
loadPhones: jest.fn()
};
const wrapper = shallow(<App {...props} />);
return {
wrapper,
props
};
};
// this is just the way I personally write my inital describe, I find it the easiest way
// to describe the component being rendered. (alot of the things below will be opinios on test improvements as well).
describe('<App />', () => {
describe('When the component has intially rendered' () => {
beforeAll(() => {
const { props } = setupFunc();
});
it ('should call loadPhones after the component has initially rendered, () => {
expect(props.loadPhones).toHaveBeenCalled();
});
});
describe('When it renders WITH props present', () => {
// we should use describes to section our tests as per how the code is written
// 1. when it renders with the props present in the component
// 2. when it renders without the props
beforeAll(() => {
const { wrapper, props } = setupFunc();
});
// "render without throwing an error" sounds quite broad or more like
// how you would "describe" how it rendered before testing something
// inside of the render. We want to have our "it" represent what we're
// actually testing; that introductoryNave has rendered with text.
it("should render an introductoryNav with text", () => {
// toContain is a bit broad, toBe would be more specific
expect(wrapper.find('.introductoryNav').text()).toBe("Phones");
});
it("should render a TablePhones component with data from props", () => {
// iirc toEqual should work here, you might need toStrictEqual though.
expect(wrapper.find('TablePhones').prop('phones')).toEqual(props.phones);
});
});
describe('When it renders WITHOUT props present', () => {
it("should render with some loading components", () => {
expect(wrapper.find('.gridLoadingContainer').exists()).toBeTruthy();
expect(wrapper.find('CircularProgress').exists()).toBeTruthy();
expect(wrapper.find('.loadingText1').exists()).toBeTruthy();
});
});
});

react-testing-library not cleaning up after fire event

We have been using React hooks useReducer and useContext to handle a global store in our react app.
When running testing using react testing library we noticed that once our state is changed inside of one test all the tests that follow now have that state change.
We have attempted doing a cleanup with afterEach(cleanup) but that did not work.
Not sure what is going on?
import React, { useContext, useReducer } from 'react'
import { render, fireEvent } from '#testing-library/react'
import '#testing-library/jest-dom/extend-expect'
import TodosList from './TodosList'
import reducer from '../../reducers/reducer'
import Store from '../../context'
import fixture from '../../tests/fixtures'
function Component() {
const [state, dispatch] = useReducer(reducer, fixture)
return (
<Store.Provider value={{ state, dispatch }}>
<TodosList />
</Store.Provider>
)
}
describe('todos', () => {
it('removes a todo when button is pressed', () => {
const { getByTestId, getAllByText } = render(<Component />)
expect(getAllByText('Delete').length).toBe(3)
window.confirm = jest.fn().mockImplementation(() => true)
fireEvent.click(getByTestId('delete-1'))
expect(window.confirm).toHaveBeenCalled()
expect(getAllByText('Delete').length).toBe(2)
})
it('check that first test did not effect this test', () => {
const { getByTestId, getAllByText } = render(<Component />)
expect(getAllByText('Delete').length).toBe(3) //this fails and is 2
})
})
I have encountered same problem and I came to the conclusion that
cleanup Unmounts React trees that were mounted with render, but doesn't reset state from stores/reducers.
In my case, since I'm using another library for central state management, my solution was to create a reset function in my store and call it at the beginning of each test. You can either use the same approach which in my opinion is closer to how the user would use the app, or follow the next one.
In the official recommendation on working with Redux they recommend recreating your own render function and provide your store to it
https://testing-library.com/docs/example-react-redux
function render(
ui,
{
initialState = reducerInitialState,
store = createStore(reducer, initialState),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
// In your test
const store = createStore(() => ({ count: 1000 }))
render(<Counter />, {
store,
});

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 useEffect using Enzyme shallow and not mount

// 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.

Resources