Jest - test if function is called if prop is present - javascript

I want to write a test to see if a useEffect function inside my component is called if the correct prop is passed in.
PropDetail.js
import React, { useState, useEffect } from "react";
function PropDetail({ propID }) {
const [propStatus, setPropStatus] = useState(null);
useEffect(() => {
if (!propID) return;
setPropStatus(propID)
}, [propID]);
return <p>{propStatus}</p>
}
export default PropDetail;
PropDetail.test.js
import React from "react";
import { shallow } from "enzyme";
import PropDetail from '../PropDetail'
const props = { }
describe('PropDetail', () => {
const wrapper = shallow(<PropDetail {...props} />)
describe('With no propID', () => {
it('returns null if no propID passed in', () => {
expect(wrapper.html()).toBe(null)
})
})
describe('With propID passed in', () => {
beforeEach(() => {
wrapper.setProps({ propID: 'PROPID' })
})
it('Runs useEffect with propID', () => {
expect(wrapper.setPropStatus().toHaveBeenCalledTimes(1);
})
})
})
The errors i'm getting in the console are 'Matcher error: received value must be a mock or spy function' and 'Received has value: undefined'.
I'm quite new to writing tests so i'm not sure if this is the right starting point or if i'm testing the wrong thing!

You don't want to test a feature of React such as useEffect. You'll want to test your code in a way such as 'what happens when useEffect has been called'.
You could test the value of propStatus upon initial rendering. Then make an update to the propID, then test the value of propStatus. Is the the value you expect or desire?
This way you're testing the actual code you wrote for correctness. When you see a change in propStatus, you'll also know that useEffect was called without directly testing that function.
You can also test for coverage by using yarn test --coverage.

Related

mocking a function thats already imported in the source

here is the basic setup
component.js
import {functionIWantToMock, val} from "somewhere/on/my/hdd"
export function Component() {
return (<>
<div>{val}</div>
<div>{functionIWantToMock()}</div>
</>)
}
component.test.js
import { Component } from "./component";
import { render, screen } from "#testing-library/react";
it("should change the result of the function", () => {
// Change behaviour of functionIWantToMock
functionIWantToMock = () => {
return "empty"
}
render(<Component/>);
// This is not a proper assertion
expect(screen.getByRole("img")).toBeVisible()
})
I've looked at manual mocking, spying and jest.fn() but none of those will work, i think, because the call is happening within the component and so I can't mock it globally.
What is the correct way to mock functionIWantToMock so that i can change its behaviour in the component for the test?
You can either use module mocking or dependency injection for this.
Module mocking
The Jest docs have an example of mocking a module, you would need to do this:
component.test.js
import { Component } from "./component";
import { render, screen } from "#testing-library/react";
// This gives you a handle to the function so you can mock
// the return value, make expectations on it, etc
import { functionIWantToMock } from "somewhere/on/my/hdd";
// This mocks all exports from the module
jest.mock("somewhere/on/my/hdd");
it("should change the result of the function", () => {
// Change behaviour of functionIWantToMock
functionIWantToMock.mockReturnValue("empty");
render(<Component/>);
expect(functionIWantToMock).toBeCalled()
})
I recommend adding clearMocks: true to your Jest config to avoid leaking state between tests.
Dependency Injection
Your other option is to pass the function into the component:
component.js
import {functionIWantToMock as defaultFn, val} from "somewhere/on/my/hdd"
// Using the import as the default function helps you avoid
// passing the function where this component is used.
export function Component({functionIWantToMock = defaultFn}) {
return (<>
<div>{val}</div>
<div>{functionIWantToMock()}</div>
</>)
}
component.test.js
import { Component } from "./component";
import { render, screen } from "#testing-library/react";
it("should change the result of the function", () => {
const mockFn = jest.fn(() => {
return "empty"
})
render(<Component functionIWantToMock={mockFn} />);
expect(mockFn).toBeCalled()
})

Preventing "not wrapped in act(...)" Jest warning when state update doesn't affect UI

I'm trying to figure out if there is a way to prevent the "not wrapped in act(...)" warning thrown by Jest/testing-library when I have nothing to assert after the state update that causes the warning happens, or if I should just ignore this warning.
Suppose I have this simple component:
import React, {useEffect, useState} from 'react';
import {getData} from 'services';
const MyComponent = () => {
const [arr, setArr] = useState([]);
useEffect(() => {
(async () => {
const {items} = await getData();
setArr(items);
})();
}, []);
return (
<div>
{!(arr.length > 0) && <p>no array items</p>}
{arr.length > 0 && (
<ul>
{arr.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
);
};
export default MyComponent;
Suppose I want to simply test that this component renders okay even if getData() doesn't return any data for me.
So I have a test like this:
import React from 'react';
import {getData} from 'services';
import {render, screen} from 'testUtils';
import MyComponent from './MyComponent';
jest.mock('services', () => ({
getData: jest.fn(),
}));
it('renders', () => {
getData.mockResolvedValue({items: []});
render(<MyComponent />);
expect(screen.getByText('no array items')).toBeInTheDocument();
});
This test will pass, but I'll get the "not wrapped in act(...)" warning because the test will finish before getData() has a chance to finish.
In this case, the response from getData() sets arr to the same value (an empty array) as I have initially set it to at the top of the component. As such, my UI doesn't change after the async function completes—I'm still just looking at a paragraph that says "no array items"—so I don't really have anything I can assert that would wait for the state update to complete.
I can expect(getData).toHaveBeenCalledTimes(1), but that doesn't wait for the state to actually be updated after the function call.
I have attempted an arbitrary pause in the test to allow time for setArr(items) to happen:
it('renders', async () => {
getData.mockResolvedValue({items: []});
render(<MyComponent />);
expect(screen.getByText('no array items')).toBeInTheDocument();
await new Promise(resolve => setTimeout(resolve, 2000));
expect(screen.getByText('no array items')).toBeInTheDocument();
});
But that doesn't seem to help, and I'm honestly not sure why.
Is there a way to handle this situation by modifying only the test?
I am sure I could fix the problem by refactoring MyComponent, e.g., by passing arr to MyComponent as a prop and moving the getData() call to a parent component, or creating some custom prop used only for testing that would skip the getData() call altogether, but I don't want to be modifying components purely to avoid warnings in tests.
I am using testing-library/react, v11.2.2.
You can use findByText (a combination of getByText and waitFor) to ensure all updates have happened when the assertion resolves.
it('renders', async () => {
getData.mockResolvedValue({items: []});
render(<MyComponent />);
expect(await screen.findByText('no array items')).toBeInTheDocument();
});
#juliomalves's answer is spot on.
However, I had to put this await in my beforeEach:
import {render, fireEvent} from '#testing-library/react';
import MyComponent from './MyComponent';
...
describe('MyComponent should', () => {
let getByText, getByTestId, getAllByTestId, getByLabelText;
beforeEach(async () => {
let findByText;
({
getByText,
getByTestId,
getAllByTestId,
getByLabelText,
findByText,
} = render(<MyComponent {...props} />));
// Have this here to avoid warnings because of setting state variables
await findByText('no array items');
})
...
});

setup function returns undefined when testing a custom hook usePrevious

I have got code for usePrevious hook from somewhere on internet. The code for usePrevious looks like:
export const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
Now, I am learing testing react with jest and enzyme. So, I tried to test usePrevious and got some problems. Here is my test case:
import React from 'react';
import { render } from 'enzyme';
import { usePrevious } from './customHooks';
const Component = ({ children, value }) => children(usePrevious(value));
const setup = (value) => {
let returnVal = '';
render(
<Component value={value}>
{
(val) => {
returnVal = val;
return null;
}
}
</Component>,
);
return returnVal;
};
describe('usePrevious', () => {
it('returns something', () => {
const test1 = setup('test');
const test2 = setup(test1);
expect(test2).toBe('test');
});
});
When the test execution completes, I get this error:
Expected: 'test', Received: undefined
Can anyone please let me know why am I getting undefined and is this the correct way to test custom hoooks in react?
After suggestion from comments from #Dmitrii G, I have changed my code to re-render the component (Previously I was re-mounting the component).
Here is the updated code:
import React from 'react';
import PropTypes from 'prop-types';
import { shallow } from 'enzyme';
import { usePrevious } from './customHooks';
const Component = ({ value }) => {
const hookResult = usePrevious(value);
return (
<div>
<span>{hookResult}</span>
<span>{value}</span>
</div>
);
};
Component.propTypes = {
value: PropTypes.string,
};
Component.defaultProps = {
value: '',
};
describe('usePrevious', () => {
it('returns something', () => {
const wrapper = shallow(<Component value="test" />);
console.log('>>>>> first time', wrapper.find('div').childAt(1).text());
expect(wrapper.find('div').childAt(0).text()).toBe('');
// Test second render and effect
wrapper.setProps({ value: 'test2' });
console.log('>>>>> second time', wrapper.find('div').childAt(1).text());
expect(wrapper.find('div').childAt(0).text()).toBe('test');
});
});
But still I am getting the same error
Expected: "test", Received: ""
Tests Passes when settimeout is introduced:
import React from 'react';
import PropTypes from 'prop-types';
import { shallow } from 'enzyme';
import { usePrevious } from './customHooks';
const Component = ({ value }) => {
const hookResult = usePrevious(value);
return <span>{hookResult}</span>;
};
Component.propTypes = {
value: PropTypes.string,
};
Component.defaultProps = {
value: '',
};
describe('usePrevious', () => {
it('returns empty string when component is rendered first time', () => {
const wrapper = shallow(<Component value="test" />);
setTimeout(() => {
expect(wrapper.find('span').text()).toBe('');
}, 0);
});
it('returns previous value when component is re-rendered', () => {
const wrapper = shallow(<Component value="test" />);
wrapper.setProps({ value: 'test2' });
setTimeout(() => {
expect(wrapper.find('span').text()).toBe('test');
}, 0);
});
});
I am not a big fan of using settimeout, so I feel that probably i am doing some mistake. If anyone knows a solution that does not use settimeout, feel free to post here. Thank you.
Enzyme by the hood utilizes React's shallow renderer. And it has issue with running effects. Not sure if it's going to be fixed soon.
Workaround with setTimeout was a surprise to me, did not know it works. Unfortunately, it is not universal approach since you'd need to use that on any change that cases re-render. Really fragile.
As a solution you can use mount() instead.
Also you may mimic shallow rendering with mount() with mocking every nested component:
jest.mock("../MySomeComponent.jsx", () =>
(props) => <span {...props}></span>
);

Testing with jest+enzyme, mock functions is called twice for no reason

I'm trying to learn Jest and Enzyme but I'm having a problem I can't find a solution to, this is my test.. it's not very good I know but I'm learning:
import * as apiMock from '../api';
const fakePostId = '1';
const fakePersona = 'Fake';
jest.mock('../api', () => {
return {
fetchAllComments: jest.fn(() => {
return [];
}),
filterComments: jest.fn(() => {
return [];
}),
createCommentObject: jest.fn(() => {
return [];
}),
};
});
test('checks if functions are called after didMount', () => {
const component = shallow(
<Comments postId={fakePostId} currentPersona={fakePersona} />
);
const spySetComments = jest.spyOn(
component.instance(),
'setCommentsFromLocalStorage'
);
component.instance().componentDidMount();
expect(spySetComments).toHaveBeenCalledTimes(1);
//Don't know why these are called 2! times, I can't see why removing componentDidMount makes it 0.
expect(apiMock.fetchAllComments).toHaveBeenCalledTimes(1);
expect(apiMock.filterComments).toHaveBeenCalledTimes(1);
}
The problem is toHaveBeenCalledTimes(1) fails with reason:
Expected mock function to have been called one time, but it was called
two times.
But I don't know why.
setCommentsFromLocalStorage only runs once, and that is the function that sohuld run from componentDidMount and execute these api calls once.
ReactComponent looks like this:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import CreateNewComment from './CreateNewComment';
import SingleComment from './SingleComment';
import * as api from '../api';
class Comments extends Component {
state = {
comments: []
};
componentDidMount() {
this.setCommentsFromLocalStorage();
}
setCommentsFromLocalStorage = (postId = this.props.postId) => {
const fetchedComments = api.fetchAllComments();
const comments = api.filterComments(fetchedComments, postId);
this.setState({ comments });
};
removeComment = commentId => {
api.removeComment(commentId);
this.setCommentsFromLocalStorage();
};
renderCommentList = (comments, currentPersona) =>
comments.map(comment => (
<SingleComment
{...comment}
currentPersona={currentPersona}
key={comment.id}
onClick={this.removeComment}
/>
));
render() {
return (
<div className="py-2">
<h2 className="text-indigo-darker border-b mb-4">Comments</h2>
{this.renderCommentList(this.state.comments, this.props.currentPersona)}
<CreateNewComment
postId={this.props.postId}
author={this.props.currentPersona}
updateComments={this.setCommentsFromLocalStorage}
/>
</div>
);
}
}
Comments.propTypes = {
postId: PropTypes.string.isRequired,
currentPersona: PropTypes.string.isRequired
};
export default Comments;
componentDidMount gets called during shallow().
This means that setCommentsFromLocalStorage gets called which calls fetchAllComments and filterComments all during that initial shallow() call.
api has already been mocked so it records those calls to fetchAllComments and filterComments.
Once that has all happened, the spy is created for setCommentsFromLocalStorage and componentDidMount gets called again (which calls fetchAllComments and filterComments again).
The spy for setCommentsFromLocalStorage then correctly reports that it was called once (since it only existed during the second call to componentDidMount).
The spies on fetchAllComments and filterComments then correctly report that they were called two times since they existed during both calls to componentDidMount.
The easiest way to fix the test is to clear the mocks on fetchAllComments and filterComments before the call to componentDidMount:
apiMock.fetchAllComments.mockClear(); // clear the mock
apiMock.filterComments.mockClear(); // clear the mock
component.instance().componentDidMount();
expect(spySetComments).toHaveBeenCalledTimes(1); // SUCCESS
expect(apiMock.fetchAllComments).toHaveBeenCalledTimes(1); // SUCCESS
expect(apiMock.filterComments).toHaveBeenCalledTimes(1); // SUCCESS
Use beforeEach and afterEach to mock and mockRestore the spies, respectively.
This is explained in the Setup and Teardown section in Jest docs.

Jest: mocking console.error - tests fails

The Problem:
I have a simple React component I'm using to learn to test components with Jest and Enzyme. As I'm working with props, I added the prop-types module to check for properties in development. prop-types uses console.error to alert when mandatory props are not passed or when props are the wrong data type.
I wanted to mock console.error to count the number of times it was called by prop-types as I passed in missing/mis-typed props.
Using this simplified example component and test, I'd expect the two tests to behave as such:
The first test with 0/2 required props should catch the mock calling twice.
The second test with 1/2 required props should catch the mock called once.
Instead, I get this:
The first test runs successfully.
The second test fails, complaining that the mock function was called zero times.
If I swap the order of the tests, the first works and the second fails.
If I split each test into an individual file, both work.
console.error output is suppressed, so it's clear it's mocked for both.
I'm sure I am missing something obvious, like clearing the mock wrong or whatever.
When I use the same structure against a module that exports a function, calling console.error some arbitrary number of times, things work.
It's when I test with enzyme/react that I hit this wall after the first test.
Sample App.js:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class App extends Component {
render(){
return(
<div>Hello world.</div>
);
}
};
App.propTypes = {
id : PropTypes.string.isRequired,
data : PropTypes.object.isRequired
};
Sample App.test.js
import React from 'react';
import { mount } from 'enzyme';
import App from './App';
console.error = jest.fn();
beforeEach(() => {
console.error.mockClear();
});
it('component logs two errors when no props are passed', () => {
const wrapper = mount(<App />);
expect(console.error).toHaveBeenCalledTimes(2);
});
it('component logs one error when only id is passed', () => {
const wrapper = mount(<App id="stringofstuff"/>);
expect(console.error).toHaveBeenCalledTimes(1);
});
Final note: Yeah, it's better to write the component to generate some user friendly output when props are missing, then test for that. But once I found this behavior, I wanted to figure out what I'm doing wrong as a way to improve my understanding. Clearly, I'm missing something.
I ran into a similar problem, just needed to cache the original method
const original = console.error
beforeEach(() => {
console.error = jest.fn()
console.error('you cant see me')
})
afterEach(() => {
console.error('you cant see me')
console.error = original
console.error('now you can')
})
Given the behavior explained by #DLyman, you could do it like that:
describe('desc', () => {
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterAll(() => {
console.error.mockRestore();
});
afterEach(() => {
console.error.mockClear();
});
it('x', () => {
// [...]
});
it('y', () => {
// [...]
});
it('throws [...]', () => {
shallow(<App />);
expect(console.error).toHaveBeenCalled();
expect(console.error.mock.calls[0][0]).toContain('The prop `id` is marked as required');
});
});
What guys wrote above is correct. I've encoutered similar problem and here's my solution. It takes also into consideration situation when you're doing some assertion on the mocked object:
beforeAll(() => {
// Create a spy on console (console.log in this case) and provide some mocked implementation
// In mocking global objects it's usually better than simple `jest.fn()`
// because you can `unmock` it in clean way doing `mockRestore`
jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterAll(() => {
// Restore mock after all tests are done, so it won't affect other test suites
console.log.mockRestore();
});
afterEach(() => {
// Clear mock (all calls etc) after each test.
// It's needed when you're using console somewhere in the tests so you have clean mock each time
console.log.mockClear();
});
You didn't miss anything. There is a known issue (https://github.com/facebook/react/issues/7047) about missing error/warning messages.
If you switch your test cases ('...when only id is passed' - the fisrt, '...when no props are passed' - the second) and add such
console.log('mockedError', console.error.mock.calls); inside your test cases, you can see, that the message about missing id isn't triggered in the second test.
For my solutions I'm just wrapping original console and combine all messages into arrays. May be someone it will be needed.
const mockedMethods = ['log', 'warn', 'error']
export const { originalConsoleFuncs, consoleMessages } = mockedMethods.reduce(
(acc: any, method: any) => {
acc.originalConsoleFuncs[method] = console[method].bind(console)
acc.consoleMessages[method] = []
return acc
},
{
consoleMessages: {},
originalConsoleFuncs: {}
}
)
export const clearConsole = () =>
mockedMethods.forEach(method => {
consoleMessages[method] = []
})
export const mockConsole = (callOriginals?: boolean) => {
const createMockConsoleFunc = (method: any) => {
console[method] = (...args: any[]) => {
consoleMessages[method].push(args)
if (callOriginals) return originalConsoleFuncs[method](...args)
}
}
const deleteMockConsoleFunc = (method: any) => {
console[method] = originalConsoleFuncs[method]
consoleMessages[method] = []
}
beforeEach(() => {
mockedMethods.forEach((method: any) => {
createMockConsoleFunc(method)
})
})
afterEach(() => {
mockedMethods.forEach((method: any) => {
deleteMockConsoleFunc(method)
})
})
}

Categories