I have this component where test coverage says I need to test lines 24 and 25:
class TableToolbarComp extends Component {
state = {
shipmentId: '',
};
debouncedSetFilters = debounce(() => {
const { applyFilters } = this.props; // LINE 24
applyFilters(this.state); // LINE 25
}, 750);
updateShipmentId = ev => {
this.setState(
{
shipmentId: ev.target.value,
},
this.debouncedSetFilters,
);
};
render() {...}
}
And the test:
beforeEach(() => {
applyFilters: k => k,
});
...
it('should trigger button click', () => {
const wrapper = shallow(<TableToolbarComp {...props} />);
wrapper.instance().debouncedSetFilters(750);
wrapper.instance().updateShipmentId({ target: { shipmentId: '124' } });
wrapper.instance().props.applyFilters({ shipmentId: '124' });
});
And I am not getting any errors, it just says those 2 lines need coverage.
I already attempted to called debouncedSetFilters and applyFilters on the test but it's still returning those 2 lines as uncover.
What am I missing?
Function calls cannot be tested efficiently without spies. It should be:
beforeEach(() => {
applyFilters = jest.fn();
});
In order to test asynchronous time-sensitive function, timer mocks should be applied:
jest.useFakeTimers();
const wrapper = shallow(<TableToolbarComp applyFilters={applyFilters} />);
wrapper.instance().debouncedSetFilters();
wrapper.instance().debouncedSetFilters();
expect(applyFilters).not.toHaveBeenCalled();
jest.advanceTimersByTime(750);
expect(applyFilters).toHaveBeenCalledTimes(1);
Then debouncedSetFilters can be stubbed in updateShipmentId test.
Related
In my test file I am mounting a Component and one of the nested Components is making me troubles. This is the Component:
class CacheHandler extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
isLatestVersion: false,
refreshCacheAndReload: () => {
if (caches) {
caches.keys().then((names) => {
names.forEach((name) => {
caches.delete(name);
})
});
}
window.location.reload(true);
}
};
// ...some other code
}
render() {
const { loading, isLatestVersion, refreshCacheAndReload } = this.state;
return this.props.children({ loading, isLatestVersion, refreshCacheAndReload });
}
}
CacheHandler.propTypes = {
children: PropTypes.func.isRequired
};
export default CacheHandler;
I do not know how properly mock the constructor's refreshCacheAndReload property that gives me grey hair. It would be totally ok if it just does not do anything in the mock, but it shoud be found during the mounting process. At the moment, when I run my test, I get because of that part ReferenceError: EventSource is not defined
This is what I tried inside of my test but failed (Error: "CacheHandler" is read-only.):
const fakeCacheHandler = jest.fn(() => ({
constructor(props) {
//super(props);
this.state = {
loading: false,
isLatestVersion: false,
refreshCacheAndReload: () => { }
}},
render() {
const { loading, isLatestVersion, refreshCacheAndReload } = this.state;
return this.props.children({ loading, isLatestVersion, refreshCacheAndReload });
}
}))
CacheHandler = fakeCacheHandler;
I also tried to define the property directly in test but without success:
Object.defineProperty(CacheHandler, 'CacheHandler', {
value: jest.fn().mockImplementation(query => ({
loading: false,
isLatestVersion: false,
refreshCacheAndReload: () => {}
}))
})
I also tried to mock the whole module in the test like this:
jest.mock('../../components/utilities/CacheHandler', function() {
return jest.fn().mockImplementation(() => {
return {
refreshCacheAndReload: () => {},
render: () => {this.props.children({ loading:false, isLatestVersion:false, refreshCacheAndReload })},
}})
});
but still not successful.
jest.spyOn fails as well(Cannot spy the refreshCacheAndReload property because it is not a function; undefined given instead)
const fakeHandler = new CacheHandler();
const methodSpy = jest.spyOn(fakeHandler, "refreshCacheAndReload");
methodSpy.mockImplementation(() => {
console.log('test!');
})
This is how the test itself look like now:
it('renders MembersList', async () => {
const Component = withMemory(AdminAppCore, ROUTE_ADMIN_MEMBERS);
const result = mount(
<MockProvider stores={{ memberStore, programStore }}>
<Component />
</MockProvider>
);
console.log(result.debug());
await waitForState(result, state => state.loading === false);
expect(result.find(MembersList).length).toBe(1);
result.unmount();
});
I tried to mock the constructor of child element inside the test like this, but if failed(TypeError: this.props.children is not a function):
it('renders MembersList', async () => {
const Component = withMemory(AdminAppCore, ROUTE_ADMIN_MEMBERS);
const mockrefreshCacheAndReload = jest.fn(() => ({}));
const component = shallow(<CacheHandler/>);
component.setState({refreshCacheAndReload: mockrefreshCacheAndReload});
const result = mount(
<MockProvider stores={{ memberStore, programStore }}>
<Component />
</MockProvider>
);
console.log(result.debug());
await waitForState(result, state => state.loading === false);
expect(result.find(MembersList).length).toBe(1);
result.unmount();
});
So I am mounting the AdminAppCore, and inside of the AdminAppCore is the nested component that causes troubles.
Can anyone please explain me how can I mock the refreshCacheAndReload state inside of the nested Component constructor?
As I cannot post this in the comments, so please try this.
const mockrefreshCacheAndReload = jest.fn(() => ({}));
it('tests the method', () => {
const component = shallow(<CacheHandler/>);
component.setState({refreshCacheAndReload: mockrefreshCacheAndReload})
})
I have this code:
const handleButton = React.useCallback(async () => {
try {
await client.clearStore();
navigation.push('screen');
} catch (e) {
logger.error('error', e);
}
}, []);
I'm testing the nagivation to screen, but with the await I can't execute or reach the navigation.push(). If I remove the await on client.clearStore it's all ok, but the behaviour could be not as expected (I need first to clearStore then to navigate).
Any idea?
Edit:
My test is like this one:
import { useApolloClient } from '#apollo/react-hooks';
// After imports
jest.mock('#apollo/react-hooks');
// In describe:
describe('text', () => {
beforeEach(async () => {
// #ts-ignore
moment.mockClear();
useApolloClient.mockClear();
});
// My test:
it.only('should redirects to MyComponent when button is pressed', async () => {
const apolloClient = {
clearStore: jest.fn()
};
useApolloClient.mockImplementation(() => apolloClient);
apolloClient.clearStore
.mockReturnValueOnce(
Promise.resolve('foo')
);
moment.mockImplementation(
(args: moment.MomentInput) =>
jest.requireActual('moment')(args || '2020-07-30T20:00:00.000Z')
);
const navigation = {
push: jest.fn()
};
const rendered = render(
<MyComponent
paymentGatewayCode='khipu'
allowToPay={true}
expiresDate={'2020-07-30T20:00:00.000Z'}
navigation={navigation}
/>
);
const button = rendered.getByTestId('button');
fireEvent.press(button);
console.log('navigation.push.mock.calls', navigation.push.mock.calls);
expect(navigation.push.mock.calls.length).toBe(1);
expect(navigation.push.mock.calls[0]).toEqual(['MyComponent']);
});
And I get this while running the test:
I have a React Component MyComponent where I want to test behavior that should be triggered when a user rotates their phone.
Inside the Component:
export class MyComponent extends React.PureComponent<props> {
componentDidMount() {
window.addEventListener('orientationchange', this.onRotation)
}
componentWillUnmount() {
window.removeEventListener('orientationchange', this.onRotation)
}
onRotation = () => {
// do things
}
render() {
// ...
}
}
I found an article on medium that describes how to write tests for this here. However, that doesn't work for me.
describe('<MyComponent />', () => {
it('does things on rotation', () => {
const map : any = {}
window.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
})
const wrapper : any = mount(<MyComponent />)
map.orientationchange()
expect(wrapper.onRotation).toHaveBeenCalled()
})
})
In the article this works, however I get an error:
"Matcher error: received value must be a mock or spy function
Received has value: undefined"
Using a spy also doesn't work:
it('does things on rotation', () => {
const map : any = {}
window.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
})
const wrapper : any = mount(<MyComponent />)
const spy = jest.spyOn(wrapper.instance(), 'onRotation')
map.orientationchange()
expect(spy).toHaveBeenCalled()
})
It says:
"Expected mock function to have been called, but it was not called."
Spy on function inside onRotation.
import React from 'react';
class OrientationChange extends React.Component {
componentDidMount() {
window.addEventListener('orientationchange', this.onRotation)
}
componentWillUnmount() {
window.removeEventListener('orientationchange', this.onRotation)
}
handleRotation = () => {
console.log('inside handle rotation');
}
onRotation = (event) => {
this.handleRotation()
}
render() {
return (
<div>Testing</div>
)
}
}
export default OrientationChange;
describe('<OrientationChange /> orientation change test', () => {
it('does things on rotation', () => {
const map = {}
window.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
})
const wrapper = mount(<OrientationChange />)
const spy = jest.spyOn(wrapper.instance(), 'handleRotation')
map.orientationchange();
expect(spy).toHaveBeenCalled()
})
})
I have this component:
// imports
class FiltersModal extends React.Component {
state = {
status: '',
carrier: '',
};
applyFilters = () => {
const { applyFilters } = this.props;
const {
status,
carrier,
} = this.state;
applyFilters({
status,
carrier,
});
};
handleChange = field => ev => {
this.setState({ [field]: ev.target.value });
};
render() {
const { t, isFiltersModalOpened, toggleFiltersModal } = this.props;
const { shippedDate } = this.state;
return (
<Modal
open={isFiltersModalOpened}
onRequestClose={toggleFiltersModal}
onRequestSubmit={this.applyFilters}
>
<Form>
<StatusesSelect handleStatus={this.handleChange('status')} />
<GetAllCouriers handleCouriers={this.handleChange('carrier')} />
</Form>
</Modal>
);
}
}
FiltersModal.propTypes = {
t: PropTypes.func.isRequired,
isFiltersModalOpened: PropTypes.bool.isRequired,
toggleFiltersModal: PropTypes.func.isRequired,
applyFilters: PropTypes.func.isRequired,
};
export default translate()(FiltersModal);
And this test:
import React from 'react';
import { shallow } from 'enzyme';
import FiltersModal from '../../FiltersModal';
jest.mock('react-i18next', () => ({
// this mock makes sure any components using the translate HoC receive the t function as a prop
translate: () => Component => {
Component.defaultProps = { ...Component.defaultProps, t: key => key }; // eslint-disable-line
return Component;
},
}));
describe('FiltersModal component test', () => {
let props;
beforeEach(() => {
props = {
t: k => k,
isFiltersModalOpened: false,
toggleFiltersModal: jest.fn(() => k => k),
removeFilter: jest.fn(() => k => k),
applyFilters: jest.fn(() => k => k),
softlayerAccountId: '232279',
filters: {
carrier: 'UPS',
shipmentId: '1234',
shipmentType: '',
shippedDate: '',
shippedFrom: '',
shippedTo: '',
status: '',
},
};
});
it('should render without errors', () => {
const wrapper = shallow(<FiltersModal {...props} />);
expect(wrapper.find('Modal')).toHaveLength(1);
expect(wrapper.find('Form')).toHaveLength(1);
});
it('should change state', () => {
const wrapper = shallow(<FiltersModal {...props} />);
wrapper.setState({ carrier: 'UPS' });
wrapper.instance().applyFilters();
wrapper.instance().handleChange('status');
expect(props.applyFilters).toHaveBeenCalledTimes(1);
expect(wrapper.instance().handleChange).toHaveBeenCalledTimes(1);
});
});
What I need is to call the function handleChange but I am getting this error:
FAIL src/client/pages/Shipments/__tests__/components/FiltersModal-test.js
FiltersModal component test
✓ should render without errors (15ms)
✕ should change state (12ms)
● FiltersModal component test › should change state
expect(jest.fn())[.not].toHaveBeenCalledTimes()
jest.fn() value must be a mock function or spy.
Received:
function: [Function anonymous]
51 |
52 | expect(props.applyFilters).toHaveBeenCalledTimes(1);
> 53 | expect(wrapper.instance().handleChange).toHaveBeenCalledTimes(1);
| ^
54 | });
55 | });
56 |
What am I missing?
I believe you don't need that. As well as in the real project you typically don't communicate with component's method from the outside(ref to call instance's methods is an exception). Instead you rely on what render() returns.
So I propose you to ensure that changed values go outsides through applyFilters:
it('should change state', () => {
const applyFiltersMock = jest.fn();
const wrapper = shallow(<FiltersModal {...props} applyFilters={applyFiltersMock} />);
wrapper.find(StatusesSelect).props().handleStatus({ target: {value: '2'} });
wrapper.find(GetAllCouriers ).props().handleCouriers({ target: {value: '3'} });
wrapper.find(Modal).props().onRequestSubmit();
expect(applyFiltersMock).toHaveBeenCalledTimes(1);
expect(applyFiltersMock).toHaveBeenCalledWith({status: '2', carrier: '3'});
});
handleChange is an actual function, not a mock or spy (as indicated by the error).
If you don't want to mock the function, you can use a spy to check if it has been called:
const spy = jest.spyOn(wrapper.instance(), "handleChange");
wrapper.instance().handleChange("status");
expect(spy).toHaveBeenCalledTimes(1);
You can check state of component by using state. After triggering handleChange check the state of component, if state is changed to desired it should pass the test
I've looked at various suggestions to solve testing a class property with no success and was wondering if anyone could possibly cast a little more light on where I may be going wrong, here are the tests I've tried all with the error Expected mock function to have been called, but it was not called.
Search.jsx
import React, { Component } from 'react'
import { func } from 'prop-types'
import Input from './Input'
import Button from './Button'
class SearchForm extends Component {
static propTypes = {
toggleAlert: func.isRequired
}
constructor() {
super()
this.state = {
searchTerm: ''
}
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit = () => {
const { searchTerm } = this.state
const { toggleAlert } = this.props
if (searchTerm === 'mocky') {
toggleAlert({
alertType: 'success',
alertMessage: 'Success!!!'
})
this.setState({
searchTerm: ''
})
} else {
toggleAlert({
alertType: 'error',
alertMessage: 'Error!!!'
})
}
}
handleChange = ({ target: { value } }) => {
this.setState({
searchTerm: value
})
}
render() {
const { searchTerm } = this.state
const btnDisabled = (searchTerm.length === 0) === true
return (
<div className="well search-form soft push--bottom">
<ul className="form-fields list-inline">
<li className="flush">
<Input
id="search"
name="search"
type="text"
placeholder="Enter a search term..."
className="text-input"
value={searchTerm}
onChange={this.handleChange}
/>
<div className="feedback push-half--right" />
</li>
<li className="push-half--left">
<Button className="btn btn--positive" disabled={btnDisabled} onClick={this.handleSubmit}>
Search
</Button>
</li>
</ul>
</div>
)
}
}
export default SearchForm
First option:
it('should call handleSubmit function on submit', () => {
const wrapper = shallow(<Search toggleAlert={jest.fn()} />)
const spy = jest.spyOn(wrapper.instance(), 'handleSubmit')
wrapper.instance().forceUpdate()
wrapper.find('.btn').simulate('click')
expect(spy).toHaveBeenCalled()
spy.mockClear()
})
Second option:
it('should call handleSubmit function on submit', () => {
const wrapper = shallow(<Search toggleAlert={jest.fn()} />)
wrapper.instance().handleSubmit = jest.fn()
wrapper.update()
wrapper.find('.btn').simulate('click')
expect(wrapper.instance().handleSubmit).toHaveBeenCalled()
})
I get that with a class property the function is an instance of the class requiring the component to be updated in order to register the function, it looks however like the component handleSubmit function gets called instead of the mock?
Swapping out handleSubmit to be a class function as a method gives me access on the class prototype which passes the test when spying on Search.prototype but I'd really like to get a solution to the class property approach.
All suggestions and recommendations would be grateful!
I suppose the unit test should be robust enough to catch the error, if case of any undesirable code changes.
Please include strict assertions in your tests.
For the conditional statements, please cover the branches as well. E.g in case of if and else statement you will have to write two tests.
For user actions, you should try to simulate the actions rather than calling the function manually.
Please see the example below,
import React from 'react';
import { shallow } from 'enzyme';
import { SearchForm } from 'components/Search';
describe('Search Component', () => {
let wrapper;
const toggleAlert = jest.fn();
const handleChange = jest.fn();
const successAlert = {
alertType: 'success',
alertMessage: 'Success!!!'
}
const errorAlert = {
alertType: 'error',
alertMessage: 'Error!!!'
}
beforeEach(() => {
wrapper = shallow(<SearchForm toggleAlert={toggleAlert} />);
});
it('"handleSubmit" to have been called with "mocky"', () => {
expect(toggleAlert).not.toHaveBeenCalled();
expect(handleChange).not.toHaveBeenCalled();
wrapper.find('Input').simulate('change', { target: { value: 'mocky' } });
expect(handleChange).toHaveBeenCalledTimes(1);
expect(wrapper.state().searchTerm).toBe('mocky');
wrapper.find('Button').simulate('click');
expect(toggleAlert).toHaveBeenCalledTimes(1);
expect(toggleAlert).toHaveBeenCalledWith(successAlert);
expect(wrapper.state().searchTerm).toBe('');
});
it('"handleSubmit" to have been called with "other than mocky"', () => {
expect(toggleAlert).not.toHaveBeenCalled();
expect(handleChange).not.toHaveBeenCalled();
wrapper.find('Input').simulate('change', { target: { value: 'Hello' } });
expect(handleChange).toHaveBeenCalledTimes(1);
expect(wrapper.state().searchTerm).toBe('Hello');
wrapper.find('Button').simulate('click');
expect(toggleAlert).toHaveBeenCalledTimes(1);
expect(toggleAlert).toHaveBeenCalledWith(errorAlert);
expect(wrapper.state().searchTerm).toBe('Hello');
});
});
So I've managed to create a working solution by first of all updating the wrapper instance and then updating the wrapper. Test now works.
Working test looks like:
it('should call handleSubmit function on submit', () => {
const wrapper = shallow(<Search toggleAlert={jest.fn()} />)
wrapper.instance().handleSubmit = jest.fn()
wrapper.instance().forceUpdate()
wrapper.update()
wrapper.find('.btn').simulate('click')
expect(wrapper.instance().handleSubmit).toHaveBeenCalled()
})
A solution that could work is;
Mock the function before shallow:
let handleSubmitMock = jest.fn();
LoginPage.prototype.handleSubmit = function() { handleSubmitMock() };
Use this to expect:
form.props.onSubmit();
expect(handleSubmitMock).toHaveBeenCalledTimes(1);
Try something like this
it('should call handleSubmit function on submit', () => {
const toggleAlert = jest.fn();
const wrapper = shallow(<Search toggleAlert={toggleAlert} />)
wrapper.setState({ searchText: 'mocky' });
wrapper.find('Button').at(0).simulate('click');
expect(toggleAlert).toHaveBeenLastCalledWith({
alertType: 'success',
alertMessage: 'Success!!!'
});
})
****Update
constructor(props) {
super(props) //you have to add props to access it this.props
this.state = {
searchTerm: ''
}
this.handleSubmit = this.handleSubmit.bind(this)
}
You shouldn't need to write unit tests for this scenario. You should be able to trust that the framework will fire the correct handlers that you've provided. A more useful test would be one which mocks the toggleAlert prop and tests the instance method handleSubmit. This is where the majority of custom logic will reside and consequently where we are most likely to find errors. Snapshot testing should be fine for anything that is part of the render function output.
A sensible test suite for this component would resemble something like the following:
describe('handleSubmit', () => {
let wrapper;
let spy;
describe('when searchTerm is "mocky"', () => {
beforeEach(() => {
spy = jest.fn();
wrapper = shallow(<SearchForm toggleAlert={spy} />);
wrapper.setState({ searchTerm: 'mocky' });
});
it('will fire spy with expected arguments', () => {
// verify that spy has not been fired prior to test
expect(spy).not.toBeCalled();
wrapper.instance().handleSubmit();
expect(spy).toBeCalled();
expect(spy).toBeCalledWith({
alertType: 'success',
alertMessage: 'Success!!!'
});
});
it('will set searchTerm to ""', () => {
expect(wrapper.state('searchTerm')).toBe('mocky');
wrapper.instance().handleSubmit();
expect(wrapper.state('searchTerm')).toBe('');
});
});
describe('when searchTerm is "something else"', () => {
beforeEach(() => {
spy = jest.fn();
wrapper = shallow(<SearchForm toggleAlert={spy} />);
wrapper.setState({ searchTerm: 'something else' });
});
it('will fire spy with expected arguments', () => {
// verify that spy has not been fired prior to test
expect(spy).not.toBeCalled();
wrapper.instance().handleSubmit();
expect(spy).toBeCalled();
expect(spy).toBeCalledWith({
alertType: 'error',
alertMessage: 'Error!!!'
});
});
});
});