Unable to spyOn imported function in compose block under componentDidMount - reactjs

Having some issues using spyOn for testing a method call inside my compose() block for my Reactjs app using recompose, redux, etc.
Basic layout is this:
// index.jsx
import { foo, baz } from './lib';
const enhance = compose(
foo(),
lifecycle({
componentDidMount() {
baz();
}
});
);
export const MyComp = (...);
const mapStateToProps = state => (...);
export connect(mapStateToProps)(enhance(MyComp));
// lib.js
export const foo = () => {
lifecycle({
componentDidMount() {
bar();
}
});
}
export const bar = () => {};
export const baz = () => {};
//index.test.jsx
import * as lib from '.libs';
describe('Test', () => {
const didMountSpy = jest.spyOn(MyComp.prototype, 'componentDidMount');
const fooSpy = jest.spyOn(lib, 'foo');
const barSpy = jest.spyOn(lib, 'bar');
const bazSpy = jest.spyOn(lib, 'baz');
const wrapper = mount(<MyComp ... />);
expect(didMountSpy).toHaveBeenCalledTimes(1); // PASS
expect(bazSpy).toHaveBeenCalledTimes(1); // PASS
expect(fooSpy).toHaveBeenCalledTimes(1); // FAIL
expect(barSpy).toHaveBeenCalledTimes(1); // FAIL
});
Strange part for me is if you look at baz(), I'm able to successfully expect the function call when it's not wrapped in another method in compse(). But I'm not able to expect foo(), bar(). My suspicion is that there is some weirdness with how enzyme/jest mocks the Reactjs lifecycle methods.
Has anyone one run into anything similar and have solutions to get spyOn to work for the nested methods under lifecycle.componentDidMount()?
Thanks!

You are supposed to use jest.mock method for mocking imports
Follow these steps:
Create a folder named __mocks__ in the same folder where lib.js is located
Create a file named lib.js in __mocks__ folder and provide a mock implementation for foo, bar and baz methods/components
In index.test.jsx do not import lib.js. Also, before you import index.js in index.test add the following line
jest.mock('path-to-lib/lib.js')
Refer: https://jestjs.io/docs/en/manual-mocks

fooSpy
fooSpy isn't called because foo gets called when enhance gets created which happens right when index.jsx is imported so foo has already been called by the time it gets wrapped in a spy to create fooSpy.
If you want to capture that foo was called you will need to delay the creation of enhance.
barSpy
bar() is defined locally in lib.js and it is being called directly within the definition of foo.
jest.spyOn replaces the module export of bar. Because the definition of foo is using bar directly and not the module export of bar, replacing the module export of bar with a spy does not affect the call within foo and barSpy is never called.
If you want to capture that bar was called you will need to use the module export of bar within the definition of foo.
Here is a working version of the test:
lib.js
import { lifecycle } from 'recompose';
import * as self from './lib'; // import the module into itself
export const foo = () =>
lifecycle({
componentDidMount() {
self.bar(); // call the module export for bar()
}
});
export const bar = () => {};
export const baz = () => {};
index.jsx
import * as React from 'react';
import { compose, lifecycle } from 'recompose';
import { foo, baz } from './lib';
export class MyComp extends React.Component {
render() { return (<div>My Comp</div>); }
}
// use a function to delay the creation of enhance()
export const initialize = () => {
const enhance = compose(
foo(),
lifecycle({
componentDidMount() {
baz();
}
})
);
return enhance(MyComp);
}
index.test.jsx
import * as React from 'react';
import { mount } from 'enzyme';
import * as lib from './lib';
import { initialize } from './index';
test('MyComp', () => {
const fooSpy = jest.spyOn(lib, 'foo');
const barSpy = jest.spyOn(lib, 'bar');
const bazSpy = jest.spyOn(lib, 'baz');
const MyComp = initialize(); // this will create enhance() which will call foo()
const didMountSpy = jest.spyOn(MyComp.prototype, 'componentDidMount');
const wrapper = mount(<MyComp />);
expect(didMountSpy).toHaveBeenCalledTimes(1); // PASS
expect(bazSpy).toHaveBeenCalledTimes(1); // PASS
expect(fooSpy).toHaveBeenCalledTimes(1); // PASS
expect(barSpy).toHaveBeenCalledTimes(1); // PASS
});

Related

Mock method from external file called by a React component

I have a React component which is calling methods defined in another file, something like this:
import { realMethod } from '../some-library';
export const MyComponent = () => {
const result = realMethod();
const field = result.someField;
/* Rest of the code... */
}
I want to write a test for MyComponent using react-testing-library and it's breaking because when it comes to the line const field = result.someField it doesn't know what it is (TypeError: Cannot read properties of undefined (reading 'someField').
So far I wrote this:
import * as React from 'react';
import { render, screen, userEvent, within } from '../../test-utils';
import { MyComponent } from './MyComponent';
import * as realMethods from '../some-library';
describe('<MyComponent />', () => {
it('should do stuff', () => {
const spyMethod = jest.spyOn(
realMethods,
'realMethod',
);
render(
<MyComponent />,
);
/* ... */
});
});
How can I mock realMethod and make it return something useful?
The spyOn itself shouldn't break your code. It looks like your realMethod does not return an object that contain someField, e.g.:
export const realMethod = () => ({ someField: 42 })
You can mock it like this:
jest.spyOn(realMethods, 'realMethod').mockReturnValue({ someField: 'mocked' });

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

Context API Unit test failing with TypeError

I have a HOC component WithContext (in a file conveniently named withContext.js) as follows
import React from 'react';
import { DEFAULT_STATE } from './store';
const MyContext = React.createContext(DEFAULT_STATE);
export function WithContext(Component) {
return function WrapperComponent(props) {
return (
<MyContext.Consumer>
{state => <Component {...props} context={state} />}
</MyContext.Consumer>
);
};
};
and a component that uses it as follows
import React from "react";
import { WithContext } from './withContext';
const MyComp = (context) => {
return (
<div className="flex dir-col" id="MyComp">
<p>This is a test</p>
</div>
)
};
export default WithContext(MyComp);
I also have a unit test that aims to test this MyComp component. The unit test follows
import React from "react";
import {shallow} from "enzyme";
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { WithContext } from './withContext';
// We need to configure our DOM
import jsdom from 'jsdom'
const {JSDOM} = jsdom;
const {document} = (new JSDOM('<!doctype html><html><body></body></html>')).window;
global.document = document;
global.window = document.defaultView
Enzyme.configure({
adapter : new Adapter()
})
beforeEach(() => {
jest.resetModules()
})
//Takes the context data we want to test, or uses defaults
const getMyContext = (context = {
state : {}
}) => {
// Will then mock the MyContext module being used in our MyComp component
jest.doMock('withContext', () => {
return {
MyContext: {
Consumer: (props) => props.children(context)
}
}
});
// We will need to re-require after calling jest.doMock.
// return the updated MyComp module that now includes the mocked context
return require('./MyComp').default;
};
describe("MyComp component loading check", () => {
test("Renders the MyComp component correctly", () => {
const MyCompContext = getMyContext();
const wrapper = shallow(<MyComp/>);
// make sure that we are able to find the header component
expect(wrapper.find(".flex").hostNodes().length).toBe(1);
});
});
However this test keeps failing with the message
TypeError: (0 , _withContext.WithContext) is not a function
};
export default WithContext(MyComp);
Can you please let me know what is wrong here ?
Thanks
Looks like you are mocking withContext with jest.doMock but the mock you are returning from the factory function does not contain a WithContext function.
Then when you require('./MyComp').default it is using the withContext mock within your MyComp module and failing when it tries to export default WithContext(MyComp); since the withContext mock does not define a WithContext function.

How to test function that passed from mapDispatchToProps (React/Redux/Enzyme/Jest)

I want to test that function passed from mapDispatchToProps was invoked when button clicking is simulated.
How to test that function which passed from mapDispatchToProps is invoked?
I tried to pass a mocked function by props, but it doesn't work. Any help will be appreciated.
Here below my fake class code and test example.
My component
// All required imports
class App extends React.Component<Props> {
render() {
const { onClick } = this.props;
return (
<>
<h1>Form</h1>
<input />
<button onClick={() => onClick()} />
</>
);
}
}
const mapStateToProps = (state) => {
return {
state
};
};
const mapDispatchToProps = (dispatch) => {
return {
onClick: () => dispatch(actions.onClick())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
My test file
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16/build/index';
import jsdom from 'jsdom';
import React from 'react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import ConnectedApp, { App } from './App';
function setUpDomEnvironment() {
const { JSDOM } = jsdom;
const dom = new JSDOM('<!doctype html><html><body></body></html>', { url: 'http://localhost/' });
const { window } = dom;
global.window = window;
global.document = window.document;
global.navigator = {
userAgent: 'node.js',
};
copyProps(window, global);
}
function copyProps(src, target) {
const props = Object.getOwnPropertyNames(src)
.filter(prop => typeof target[prop] === 'undefined')
.map(prop => Object.getOwnPropertyDescriptor(src, prop));
Object.defineProperties(target, props);
}
setUpDomEnvironment();
configure({ adapter: new Adapter() });
const mockStore = configureMockStore();
describe('App', () => {
describe('When App connected to store', () => {
describe('When button clicked', () => {
it('should not crush after click on login button', () => {
const onClick = jest.fn()
const store = mockStore(initialStates[1]);
const wrapper = mount(
<Provider store={store}>
<ConnectedApp />
</Provider>);
wrapper.find('button').simulate('click');
??? how to test that function passed from mapDispatchToProps was fired?
});
});
});
});
I recommend following the approach described in the docs and export the connected component as the default export for use in the application, and export the component itself as a named export for testing.
For the code above export the App class and test the click like this:
import * as React from 'react';
import { shallow } from 'enzyme';
import { App } from './code';
describe('App', () => {
it('should call props.onClick() when button is clicked', () => {
const onClick = jest.fn();
const wrapper = shallow(<App onClick={onClick} />);
wrapper.find('button').simulate('click');
expect(onClick).toHaveBeenCalledTimes(1);
});
});
shallow provides everything that is needed for testing the component itself. (shallow even calls React lifecycle methods as of Enzyme v3)
As you have found, to do a full rendering of the component requires a mock redux store and wrapping the component in a Provider. Besides adding a lot of complexity, this approach also ends up testing the mock store and all child components during the component unit tests.
I have found it much more effective to directly test the component, and to export and directly test mapStateToProps() and mapDispatchToProps() which is very easy since they should be pure functions.
The mapDispatchToProps() in the code above can be tested like this:
describe('mapDispatchToProps', () => {
it('should dispatch actions.onClick() when onClick() is called', () => {
const dispatch = jest.fn();
const props = mapDispatchToProps(dispatch);
props.onClick();
expect(dispatch).toHaveBeenCalledWith(actions.onClick());
});
});
This approach makes unit testing the component very simple since you can pass the component props directly, and makes it very simple to test that the component will be handed the correct props by the pure functions (or objects) passed to connect().
This ensures that the unit tests are simple and targeted. Testing that connect() and redux are working properly with the component and all of its child components in a full DOM rendering can be done in the e2e tests.

Testing Connected Container methods in Jest + Enzyme

I have a connected container like so:
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { autobind } from 'core-decorators';
const propTypes = {
data: PropTypes.object,
userData: PropTypes.object,
};
class ConnectedContainer extends React.Component {
#autobind
doSomethingImportant() {
...
}
render() {
....
}
}
ConnectedContainer.propTypes = propTypes;
const mapStateToProps = state => ({ data, userData });
const mapDispatchToProps = (dispatch) => {
return { actions: bindActionCreators(actions, dispatch) };
};
export default connect(mapStateToProps, mapDispatchToProps)(ConnectedContainer);
I want to test doSomethingImportant, so I have test like so:
import React from 'react';
import { shallow } from 'enzyme';
import ConnectedContainer from '.....'';
import configureStore from 'redux-mock-store';
const mockStore = configureStore();
const store = mockStore({ getState: () => null, dispatch: () => null, data: { data from fixture }, userData: { data from fixture } });
const container = (
<ConnectedContainer
store={store}
actions={{}}
/>
);
describe('ConnectedContainer', () => {
describe('doSomethingImportant', () => {
it('returns something important', () => {
const wrapper = shallow(container);
expect(wrapper.instance().doSomethingImportant()).to.equal( ... some output here );
});
});
});
And no matter what I do, I get this error:
TypeError: wrapper.instance(...). doSomethingImportant is not a function
What is happening with my container that I'm unable to access its instance methods?
Test the unwrapped component
Export the ConnectedContainer class itself, not the wrapped version. You want to test your code, not the connect() function.
You can keep the default export as the wrapped component, and then add the word export in front of the class definition:
export class ConnectedContainer extends React.Component { // Named export, for testing purposes only
...
}
Then in your test, import { ConnectedContainer } from '....'
Render that with shallow instead of the default export.
Naming conventions
Also, naming your component ConnectedContainer is very confusing! It only becomes connected after it is wrapped with the connect function. So when you export ConnectedContainer (as I've suggested) you're actually exporting a non-connected component class. (And is it actually a container? That's also pretty vague.)
One naming convention people use is to define a constant that holds the return value of connect(), and name that xxxContainer, like so:
export class IconView extends React.Component { // Exported for testing
// ... class implementation
}
function mapStateToProps(){...}
function mapDispatchToProps(){...}
const IconViewContainer = connect(mapStateToProps, mapDispatchToProps)(IconView);
export default IconViewContainer; // Exported for use as a component in other views
Try:
const Container = (
<ConnectedContainer
store={store}
actions={{}}
/>
);
Note, the only difference is the C uppercase in container. The thing is that React only treats classes and methods as components if they start with a capital letter. That could be your problem.

Resources