setState value used in componentDidMount is not reflected in Enzyme test - reactjs

Component.js
import React from 'react'
import request from 'superagent'
export default React.createClass({
getInitialState() {
return {cats: []}
},
componentDidMount() {
request('/api', (err, res) => {
if (err) return;
this.setState({
cats: res.body.results
})
})
},
render() {
let cats = this.state.cats
let catsList = (
<ul>
{cats.map((c) => <li key={c.id}>cat</li>)}
</ul>
)
return (
<div>
{cats.length ? catsList : null}
</div>
)
}
})
Component.test.js
jest.unmock('../app.js')
jest.unmock('superagent')
import React from 'react'
import {mount} from 'enzyme'
import nock from 'nock'
import App from '../app.js'
describe('App', () => {
let ajaxFn
beforeEach(() => {
ajaxFn = nock('http://localhost')
.get('/api')
.reply(200, {
results: [{id: 1}, {id: 2}, {id: 3}]
})
})
it('renders correct number of cats', () => {
let wrapper = mount(<App />)
expect(wrapper.state('cats').length).toBe(3)
})
})
The test does not pass. wrapper.state('cats').length is always 0.
I understand that setState doesn't guarantee to update state immediately,
however if I log 'cats' in the component, I can see it updating.

If you end up setting state in your component in some context that enzyme doesn't know about, you will have to manually call .update() on the wrapper in order for it to get the updated version of the render tree.
it('renders correct number of cats', () => {
let wrapper = mount(<App />)
expect(wrapper.update().state('cats').length).toBe(3)
})

I had a similar problem and it was necessary to return a promise from the it callback and check the expectation in the then method of the promise.
In your case (assuming ajaxFn was a promise, or you could turn it into one) I think this would be approximately:
it('renders correct number of cats', () => {
let wrapper = mount(<App />)
return ajaxFn.then(() => {
expect(wrapper.state('cats').length).toBe(3);
}
})

I am not familiar with all the libraries you are using, but since your code is being executed asynchronously the test is finishing before the state can be updated. I was able to solve this problem by adding async/await to the test:
it('renders correct number of cats', async () => {
let wrapper = await mount(<App />)
expect(wrapper.state('cats').length).toBe(3)
})

Related

Mocking a promise inside useEffect with CRA, React Testing Library and Jest

I am having an issue mocking a returned Promise using:
Create React app
Jest
RTL
I have a file:
const books = [{
id: 1,
name: 'book'
}];
export const getBooks = () =>
new Promise((res) => res(books));
I have a useEffect in my app:
export const App = () => {
const [books, setBooks] = useState(undefined);
useEffect(() => {
const fetchData = async () => {
try {
const response = await getBooks();
setBooks(response);
} catch (error) {
setError("There seems to be an issue. Error:", error);
}
};
fetchData();
}, []);
return (
<div>
{books &&
books.map((book) => {
return (
<li key={book.id}>
{book.name}
</li>
);
})
}
</div>
I have a test:
import { App } from './App';
import { getBooks } from './books';
jest.mock('./books', () => ({
getBooks: jest.fn(),
}));
getBlocks.mockReturnValue(() => Promise.resolve([{
id: 1,
name: 'mock book'
}]));
describe('App', () => {
it('should render blocks', async () => {
await render(<App />);
expect(screen.getByText('mock book')).toBeInTheDocument();
});
});
I just can't mock the return value! I can assert it's been called and I can console log the getBooks to see that it's mocked I just can't get any results. I also want to reject it so I can test the unhappy path but it won't work. Any ideas?
Few things:
You have a typo, it's not getBlocks but getBooks.
The await keyword is not necessary before rendering the component with render.
getBooks returns a promise that resolves with the value of books, yet when you're trying to mock it, you are making it return a function that returns a promise. Very different things.
You have to move the mocking to the test block in which it'll be used, or if you need this mocked value from getBooks on each one of your tests, you can move it inside a beforeEach hook. You can always override it for a specific test in which you are testing some edge case (e.g. an exception being thrown by the function, A.K.A "Unhappy path").
On the component's first render, books will be undefined, so you need to wait for the state to be updated. getByText query won't work, since it will immediately throw an error because it won't find the text you're expecting. You need to use the findByText query for this. It returns a promise that resolves when an element that matches the given query is found and rejects if the element is not found after the default timeout of 1000ms.
Since getBooks returns a promise, it makes more sense to use mockResolvedValue instead of mockReturnValue.
import { render, screen } from "#testing-library/react"
import { App } from "./App"
import { getBooks } from "./books"
jest.mock("./books", () => ({
getBooks: jest.fn()
}))
describe("App", () => {
it("should render blocks", async () => {
getBooks.mockResolvedValueOnce([{ id: 1, name: "mock book" }])
render(<App />)
expect(await screen.findByText("mock book")).toBeInTheDocument()
})
})
Try this:
jest.mock('./books', () => ({
getBooks: jest.fn().mockReturnValue(() => Promise.resolve([{
id: 1,
name: 'mock book'
}]));
}));

How to test functions used in a stateless component that's defined outside of the component's scope in Jest/Enzyme?

I'm trying to write a test for the following:
import React from 'react'
import Popup from 'some-library'
const popupConfig = {
home: {
popupValue: 'Hello World',
popupValue: 'action',
popupMessage: 'Get Started'
},
settings: {
popupValue: 'Hello World',
popupValue: 'action',
popupMessage: 'Get Started'
}
}
const closePopup = () => {
Popup.closePopup()
}
const toggleNewPopup = () => {
Popup.togglePopup('some-popup')
}
const GetStartedPopup = ({ moduleName }) => {
if (!Object.keys(popupConfig).includes(moduleName)) return null
const {
popupValue = 'Hi there!',
popupStyle = 'warning',
popupMessage = 'Get Started',
popupBtnFunction = toggleNewPopup
} = popupConfig[moduleName]
return (
<Popup
popupValue={popupValue}
popupStyle={popupStyle}
popupBtnValue={popupMessage}
popupBtnStyle="neutral"
popupBtnFunction={popupBtnFunction}
xPopup={closePopup}
/>
)
}
export default GetStartedPopup
The objective of the test is to make sure that the closePopup and toggleNewPopup functions are called. I'm doing the following to do that for the closePopup function:
import React from 'react'
import { mount } from 'enzyme'
import { Popup } from 'some-library'
import GetStartedPopup from 'widgets/getStartedPopup'
describe('<GetStartedPopup/>', () => {
let wrapper
let props
beforeEach(() => {
props = {
page: 'home'
}
wrapper = mount(<GetStartedPopup {...props}/>)
})
it('should render the component without crashing', () => {
expect(wrapper).toBeDefined();
})
it('should call closePopup', () => {
const spy = jest.spyOn(wrapper.instance(), 'closePopup');
wrapper.instance().closePopup();
expect(spy).toHaveBeenCalledTimes(1);
})
afterEach(() => {
wrapper.unmount()
})
})
I went through the docs for spyOn and other SO threads that tackle issues like this but couldn't resolve how to test the closePopup and toggleNewPopup functions for my case here. When I run the test case written above I get this: TypeError: Cannot read property 'closePopup' of null. What would be the correct way to write the test to make sure that the two functions are called?
Funny that I ran into this myself at work in regards to wrapper.instance() doc
To return the props for the entire React component, use wrapper.instance().props. This is valid for stateful or stateless components in React 15.. But, wrapper.instance() will return null for stateless React component in React 16., so wrapper.instance().props will cause an error in this case.
As for the 3rd party library. You should be mocking any collaborators that your component uses.
import { Popup } from 'some-library';
describe('<GetStartedPopup />', () => {
let wrapper;
jest.mock('some-library', () => {
Popup: jest.fn(),
});
const initialProps = {
page: 'home'
};
const getStartedPopup = () => {
return mount(<GetStartedPopup {...initialProps});
};
beforeEach(() => {
Popup.mockClear()
wrapper = getStartedPopup();
};
it('should call closePopup', () => {
expect(Popup.closePopup()).toHaveBeenCalledTimes(1);
});
...
});

Enzyme onclick spy toHaveBeenCalled test does not work when testing on arrow function

how can i test the child component onclick.
Please see the below snippet.
// App.js
import React, {Component, Fragment} from 'react'
import Child from './child'
class App extends Component{
state = {
data: null,
enable: false
}
componentDidMount(){
this.getData()
}
getData = async () => {
const response = await fetch('http://www.example.com');
const data = await response.json();
this.setState({
data
})
}
_handleChildClick = () => {
this.setState({
enable: true
})
}
render(){
const {data, enable} = this.state
if(!data){
return (
<div>
Loading
</div>
)
}else{
<Fragment>
<Child
handleChildClick={this._handleChildClick}
/>
</Fragment>
}
}
}
export default App
import React from 'react';
const child = () => {
return(
<div>
<button
className="toggle"
onClick={props.handleChildClick}
>
Toggle
</button>
</div>
)
}
export default child
// App.test.js
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it('should trigger _handleChildClick', async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () => new Promise((resolve, reject) => {
resolve(
{
name: "some data"
}
)
})
}))
const mountWrapper = await mount(<App />)
setTimeout(() => {
mountWrapper.update()
const SpyhandleChildClick = jest.spyOn(mountWrapper.instance(),'_handleChildClick')
mountWrapper.find('.toggle').simulate('click')
expect(SpyhandleChildClick).toHaveBeenCalled() // not called
},0)
})
})
Some important points to consider.
Asynchronous code in your tests
If you have to do asynchronous tasks in your tests you always have to await until the asynchronous stuff is completed.
setTimeout(() => {
mountWrapper.update()
const SpyhandleChildClick = jest.spyOn(mountWrapper.instance(),'_handleChildClick')
mountWrapper.find('.toggle').simulate('click')
expect(SpyhandleChildClick).toHaveBeenCalled() // not called
},0)
Above in your code you have a timeout segment. Any test condition inside this code block will not be evaluated since by the time they are evaluated you 'test session' will already be over due to the aync nature.
Testing arrow functions in React with enzyme - forceUpdate()
There seem to be a problem with the enzyme library where you have to force update the react component after spying for it to latch on to the method.
Please follow the github issue for more information : https://github.com/airbnb/enzyme/issues/365
I also cleaned up your test code a bit to make it more understandable!
// App.test.js
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it("should trigger _handleChildClick", async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () =>
new Promise((resolve, reject) => {
resolve({
name: "some data"
});
})
}));
const mountWrapper = mount(<App />);
mountWrapper.update();
console.log("mountWrapper", mountWrapper.debug()); // showing the loader one
//[FIX]This code will block and wait for your asynchronous tasks to be completed
await new Promise(res => setTimeout(() => res(), 0));
mountWrapper.update();
console.log("mountWrapper", mountWrapper.debug()); // nothing showing
expect(mountWrapper.find(".toggle").length).toEqual(1);
//[FIX]Get a reference from the wrapper and force update after the spyOn call
const instance = mountWrapper.instance();
const spy = jest.spyOn(instance, "_handleChildClick");
instance.forceUpdate();
mountWrapper.find(".toggle").simulate("click");
expect(spy).toHaveBeenCalled();
});
});
Live Demo Link: Click on the 'Tests' tab on the browser to see the test results
https://codesandbox.io/s/mz21kpm37j

window.history.back() not being called - jest enzyme

I wrote a simple unit test for the following. I am new to React JS testing - Trying to run a test using jest and enzyme.
render() {
return (
<div>
<div className="not-found">
<div className='_2'>WAS NOT FOUND</div>
<div onClick={() => {window.history.back()}} className='not-found-
btn' href='/'>GO BACK</div>
)
}
}
The file looks simple, there are no props and the only thing not being covered when the test is running is onClick . How could I test onClick and make sure the test is 100 % covered. Thanks
<div onClick={() => {window.history.back()}} className='not-found-
btn' href='/'>GO BACK</div>
file.test.js
// jest mock functions (mocks this.props.func)
const onClick = jest.fn();
// defining this.props
const baseProps = {
onClick,
}
describe(' Test', () => {
let wrapper;
let tree;
beforeEach(() => wrapper = shallow(<Component{...baseProps } />));
// before each test, shallow mount the Component
it('should render correctly', () => {
tree = renderer.create(<NotFound {...baseProps} />)
let treeJson = tree.toJSON()
expect(treeJson).toMatchSnapshot();
tree.unmount()
});
it('calls onClick event ', () => {
const mockOnClick = jest.fn();
const wrapper = shallow(
<NotFound onClick={mockOnClick} className='not-found-btn' />
);
const component = wrapper.shallow();
component.find('GO BACK').simulate('click');
expect(mockOnClick.mock.calls.length).toEqual(1);
I'd avoid using window history and instead use react-router-dom for MPAs. In addition, instead of using an anonymous function, you can use a PureComponent class (it's similar to a Component class, but it doesn't update state) with a method class function.
Working example: https://codesandbox.io/s/j3qo6ppxqy (this example uses react-router-dom and has a mix of integration and unit testing -- see the tests tab at the bottom of the page to run the tests and look for __test__ folders to see the code)
components/NotFound/notfound.js
import React, { PureComponent } from "react";
import { Button } from "antd";
export default class NotFound extends PureComponent {
handlePageBack = () => this.props.history.push("/");
render = () => (
<div className="notfound">
<h1>404 - Not Found!</h1>
<Button type="default" onClick={this.handlePageBack}>
Go Back
</Button>
</div>
);
}
components/NotFound/__tests__/notfound.test.js (as mentioned here, you can also test the class method, if desired)
import React from "react";
import { shallowComponent } from "../../../tests/utils";
import NotFound from "../notfound";
const mockGoBack = jest.fn();
const initialProps = {
history: {
goBack: mockGoBack
}
};
/*
the shallowComponent function below is a custom function in "tests/utils/index.js" that
simplifies shallow mounting a component with props and state
*/
const wrapper = shallowComponent(<NotFound {...initialProps} />);
describe("Not Found", () => {
it("renders without errors", () => {
const notfoundComponent = wrapper.find("div.notfound");
expect(notfoundComponent).toHaveLength(1);
});
it("pushes back a page when Go Back button is clicked", () => {
wrapper.find("Button").simulate("click");
expect(mockGoBack).toHaveBeenCalled();
});
});
window.history.back is being called, but it has a delay time. I can make it work using a Promise:
const Component = ()=> (<div>
<button onClick={()=> window.history.back()} className="btn btn-back">
Back
</button>
</div>)
Component.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
const delayAction = (fn, time = 1000) =>
new Promise((resolve) => {
fn();
setTimeout(() => {
resolve();
}, time);
});
let container = null;
describe("App tests", () => {
afterEach(() => {
//unmount Component...
});
beforeEach(() => {
//mount Component
});
it("should call history.back()", async (done) => {
const btnBack = container.querySelector(".btn-back");
await act(() =>
delayAction(() => btnBack.dispatchEvent(new MouseEvent("click", { bubbles: true })))
);
// asserts..
done();
});
});

How to test componentWillReceiveProps method in react component

I want to test the componentWillReceiveProps method and see the path for current web page. I tried to use the following code to test the method, but it always throw an error.
Invariant Violation: A <Router> may have only one child element
I'm wondering what I should do to solve that error? Here is what I have tried so far.
class WrappedComponent extends React.Component {
componentWillReceiveProps(nextProps) {
if (!nextProps.user_id) {
this.props.history.replace('/login');
}
}
render() {
return <div>WrappedComponent</div>
}
}
describe('AuthenticationHOC', () => {
describe('authenticationRequired', () => {
let props;
const shallowWrapper = () => {
return shallow(
<MemoryRouter>
withRouter(authenticationRequired(<WrappedComponent {...props} />))
</MemoryRouter>
)
}
it('renders the wrapped component', () => {
let wrapper = shallowWrapper()
expect(wrapper.contains(<WrappedComponent {...props} />)).toBe(true)
})
describe("when user_id doesn't exist", () => {
beforeEach(() => {
props = {
user_id: ''
}
});
it('should go to the login page', () => {
//how to test the method componentWillReceiveProps
let wrapper = shallowWrapper().dive();
wrapper.setProps({
user_id: '12312412'
});
// expect(spy.calledOnce).toBe(true);
expect(window.href).toBe("/login");
})
})
describe("when user_id do exist", () => {
beforeEach(() => {
props = {
user_id: 'something'
}
});
it('should not go to other page', () => {
let wrapper = shallowWrapper();
expect(window.href).toBe("/");
})
})
})
You shouldn't mock componentWillReceiveProps as this is an implemntation detail that we don't care about.
Instead you will need to mock out history somehow (not sure, look at react-router docs maybe) or hopefully you can just check the current href as hopefully when you use history.replace it might just change the href immediately.
Use enzymes dive() to dive through your Higher order components and use setProps on the wrapper component.
You might need to chain dive() again depending on how many HOC's you have wrapped.
// TODO add tests that verify history.replace was called
describe("when user_id doesn't exist", () => {
beforeEach(() => {
props.user_id = ''
});
const wrapper = shallowWrapper().dive();
const user_id = 'testId';
wrapper.setProps({ user_id });
expect(window.href).toBe('/login');
})

Resources