When I create a test for my connected React component where I want to test the mapStateToProps logic I run into a problem which I'm not sure how to solve.
Error message
Expected: 1
Received: undefined
24 | it('should show previously rolled value', () => {
25 | // test that the state values were correctly passed as props
> 26 | expect(wrapper.props().lastRolledNumber).toBe(1);
When I check the wrapper.props() it only returns the store object and no properties.
To make sure it's not my code I have found an example that should work to simplify it, but I get the same problem when using that exact version in my app (option #2, https://jsramblings.com/2018/01/15/3-ways-to-test-mapStateToProps-and-mapDispatchToProps.html)
I think it might have to do with the React 16+ version, which I found mentioned here: https://airbnb.io/enzyme/docs/api/ReactWrapper/props.html
.props() => Object
Returns the props object for the root node of the wrapper. It must be
a single-node wrapper. This method is a reliable way of accessing the
props of a node; wrapper.instance().props will work as well, but in
React 16+, stateless functional components do not have an instance.
See .instance() => ReactComponent
Does anyone know how to test this in a good way to see that the logic is working as expected without exporting the private mapStateToProps function directly?
Component
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
// Component 1 - "Base component"
// Exporting it is a good practice for testing its own logic
export const Dice = ({ lastRolledNumber, onRollDice }) => (
<div>
<p>The last rolled number was {lastRolledNumber}.</p>
<button onClick={onRollDice}>Roll dice</button>
</div>
);
Dice.propTypes = {
lastRolledNumber: PropTypes.number.isRequired,
onRollDice: PropTypes.func.isRequired
}
const mapStateToProps = (state) => ({
lastRolledNumber: state.lastRolledNumber
});
const mapDispatchToProps = (dispatch) => ({
onRollDice: () => dispatch({ type: 'ROLL_DICE' })
});
// Component 2 - Container component
// Export it as a default export
export default connect(mapStateToProps, mapDispatchToProps)(Dice);
Test
import React from 'react';
import { shallow } from 'enzyme';
import '../test-config'; // Setup Enzyme & Adapter
import DiceContainer from './Dice';
// Create the mock store
import configureMockStore from 'redux-mock-store';
const mockStore = configureMockStore();
describe('Dice', () => {
let wrapper, store;
beforeEach(() => {
const initialState = {
lastRolledNumber: 1
};
store = mockStore(initialState);
// Shallow render the container passing in the mock store
wrapper = shallow(
<DiceContainer store={store} />
);
});
it('should show previously rolled value', () => {
// test that the state values were correctly passed as props
expect(wrapper.props().lastRolledNumber).toBe(1);
});
it('should roll the dice again when button is clicked', () => {
// test that the component events dispatch the expected actions
wrapper.simulate('rollDice');
const actions = store.getActions();
expect(actions).toEqual([ { type: 'ROLL_DICE' } ]);
});
});
You are almost there. You need to call .dive() method so that you can get the Dice component rather than the DiceContainer component which is wrapped by the connect HOC of react-redux module.
Short answer:
wrapper = shallow(<DiceContainer store={store} />).dive();
A Completed working example:
index.jsx:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
export const Dice = ({ lastRolledNumber, onRollDice }) => (
<div>
<p>The last rolled number was {lastRolledNumber}.</p>
<button onClick={onRollDice}>Roll dice</button>
</div>
);
Dice.propTypes = {
lastRolledNumber: PropTypes.number.isRequired,
onRollDice: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
lastRolledNumber: state.lastRolledNumber,
});
const mapDispatchToProps = (dispatch) => ({
onRollDice: () => dispatch({ type: 'ROLL_DICE' }),
});
export default connect(mapStateToProps, mapDispatchToProps)(Dice);
index.test.jsx:
import React from 'react';
import { shallow } from 'enzyme';
import configureMockStore from 'redux-mock-store';
import DiceContainer from '.';
const mockStore = configureMockStore();
describe('Dice', () => {
let wrapper;
let store;
beforeEach(() => {
const initialState = {
lastRolledNumber: 1,
};
store = mockStore(initialState);
wrapper = shallow(<DiceContainer store={store} />).dive();
});
it('should show previously rolled value', () => {
expect(wrapper.props().lastRolledNumber).toBe(1);
});
it('should roll the dice again when button is clicked', () => {
wrapper.simulate('rollDice');
const actions = store.getActions();
expect(actions).toEqual([{ type: 'ROLL_DICE' }]);
});
});
Unit test results:
PASS src/stackoverflow/59771991/index.test.jsx (9.645s)
Dice
✓ should show previously rolled value (19ms)
✓ should roll the dice again when button is clicked (2ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 11.505s
Test coverage html report:
In my case, I was able to write the test with render by this -
import React from 'react';
import '#testing-library/jest-dom';
import {
screen, cleanup, render,
} from '#testing-library/react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
const store = configureMockStore()({
headerState: {
showBack: false,
},
});
const mockProps = { ... };
describe('Component', () => {
beforeAll(() => {
render(
<Provider store={store}>
<Component {...mockProps} />
</Provider>,
);
});
afterAll(cleanup);
test('Test', () => {
screen.debug();
});
});
Related
I am trying to unit test React container component using chai/sinon. This is how the component is defined:
import { connect } from 'react-redux';
import React, { Component } from 'react';
class TestComponent extends Component {
componentDidMount() {
const { loadData } = this.props;
loadData();
}
render() {
const { links, heading } = this.props;
return (
<p>
<h1>{heading}</h1>
<div>{links.map(link => link.label)}</div>
</p>
);
}
}
const mapStateToProps = state => ({
heading: state.heading,
links: state.links
});
const mapDispatchToProps = dispatch => ({
loadData: dispatch('LOAD_DATA')
});
export default connect(mapStateToProps, mapDispatchToProps)(TestComponent);
I am writing a unit test to assert that container component has the props loadData, heading and links. Below is the unit test:
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import React from 'react';
import { shallow } from 'enzyme';
import TestContainer from './TestContainer';
const mockStore = configureStore({});
describe('TestContainer', () => {
const initialState = {
heading: 'A test component',
links: []
};
const store = mockStore(initialState);
it(
'renders correctly',
sinon.test(() => {
const component = shallow(
<Provider store={store}>
<TestContainer />
</Provider>
);
console.log(component.prop('heading')); // prints undefined
expect(component.prop('heading')).to.equal('A test component'); // gives AssertionError: expected undefined to equal 'A test component'
})
);
});
Above unit test fails and I get all props as undefined. Can somebody explain what am I doing wrong OR how a container component props should be asserted?
Thanks.
I cannot have my state when dispatch an action inside my jest unit test
I try to make like this :
import configureMockStore from 'redux-mock-store'
import * as React from 'react'
import MarginPage from './index'
import { Provider } from 'react-redux'
import {
mount,
} from 'enzyme'
import thunk from 'redux-thunk'
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
const state = {
margin: {
list: null,
},
}
it('Should list props are correctly filling', async () => {
await store.dispatch({
type: 'SET_MARGIN_LIST',
payload: [0, 1, 2],
})
const wrapper = mount(
<Provider store={store}>
<MarginPage />
</Provider>,
)
wrapper.update()
const actions = store.getActions() // Here I have my state
// But now I would like to have the updated state list inside getState()
console.log(store.getState().margin.list) // return undefined
})
I found an answer I hope its helping you if you encountered this problem.
We can take the above code and just update :
import * as React from 'react'
import MarginPage from './index'
import { Provider } from 'react-redux'
import {
mount,
} from 'enzyme'
// #ts-ignore
import store from '#App/helpers/configureStore' // Finally I prefer use store from my app
import { IAppState } from '#App/reducers' // This line come from interface of typeScript
describe('<MarginPage /> Component', () => {
it('Should list props are correctly filling', async () => {
// instanciate component and make the test use async/await
const wrapper = mount(
<Provider store={store}>
<MarginPage />
</Provider>,
)
await store.dispatch({
type: 'SET_MARGIN_LIST',
payload: MOCK_MARGIN_LIST,
})
// Don't forget to force the re-render
wrapper.update()
// Now we have access to our store
const state = (store.getState() as IAppState).margin.list as any
expect(state.results[0].campaigns_revenue).toEqual(47096.52)
})
})
I want to start my React microapp with props I'm passing from Single SPA (customProps). The only way I've figured out is:
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './where/my/root/is.js';
function domElementGetter() {
return document.getElementById("mounting-node")
}
let EnhancedRootComponent = App; /* 1 */
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: EnhancedRootComponent, /* 1 */
domElementGetter,
})
export const bootstrap = [
(args) => {
/* 2 */ EnhancedRootComponent = () => <App myArgs={args.thePropsIWannaPass} />;
return Promise.resolve();
},
reactLifecycles.bootstrap,
];
export const mount = [reactLifecycles.mount];
export const unmount = [reactLifecycles.unmount];
This does work (I can see and use the passed props in my component) but I'm not completely OK with the fact that the root component changes in between calling singleSpaReact (1) and calling bootstrap(2). Would there be side effects to this that I'm not seeing now? Does anyone know a better approach for this?
You have this value inside the props variable without this reassign.
Check this out:
Root-config.js, file responsible for passing prop to microfrontend
import { registerApplication, start } from 'single-spa';
import * as isActive from './activity-functions';
registerApplication('#company/micro2', () => System.import('#company/micro2'), isActive.micro2);
registerApplication('#company/micro1', () => System.import('#company/micro1'), isActive.micro1, { "authToken": "test" });
start();
micro1 Root.tsx
import React from 'react';
export default class Root extends React.Component {
constructor(props: any){
super(props)
}
state = {
hasError: false,
};
componentDidCatch() {
this.setState({ hasError: true });
}
render() {
console.log(this.props)
return (
<div>test</div>
);
}
}
console.log output:
props:
authToken: "test" <---- props which you pass
name: "#company/micro1"
mountParcel: ƒ ()
singleSpa: {…}
__proto__: Object
for more advance usage
const lifecycles = singleSpaReact({
React,
ReactDOM,
loadRootComponent: (props) =>
new Promise((resolve, reject) => resolve(() =>
<Root {...props} test2={'test2'}/>)),
domElementGetter,
});
I have tried many solutions I found on google to test if Component.propTypes was set properly at a react component, but none of them worked for me. Even though I get a console warning when running my React application on the browser if the properties are passed incorrectly, when I run jest I can't capture that warning in any way that I tried. Here is my best attempt:
App.js:
export class App extends Component {
constructor(props) {
super(props);
}
render() {
return <div/>;
}
}
App.propTypes = {
images: PropTypes.array.isRequired
};
function mapStateToProps(state) {
const {images} = state;
return {images: images};
}
export default connect(mapStateToProps)(App);
App.test.js:
import React from 'react';
import chai from 'chai';
import chaiEnzyme from 'chai-enzyme';
import {shallow} from 'enzyme';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import sinon from 'sinon'
import {Provider} from 'react-redux';
import App from './App';
const expect = chai.use(chaiEnzyme()).expect
const mockStore = configureStore([thunk]);
const wrap = (initialState, props) => {
return shallow(<Provider store={mockStore(initialState)}><App {...props} /></Provider>)
};
describe('App container', () => {
it('validates properties', () => {
const stub = sinon.stub(console, 'warn');
console.warn.reset();
React.createElement(App, {});
expect(stub.calledOnce).to.equal(true);
expect(stub.calledWithMatch(/Failed prop type/)).to.equal(true);
console.warn.restore();
});
it('renders without crashing', () => {
wrap();
});
it('is react-redux connected', () => {
const wrapper = wrap();
expect(wrapper.find('Connect(App)')).to.have.length(1);
});
it('correctly maps properties', () => {
const wrapper = wrap({images: []});
expect(wrapper.props().images).to.equal([]);
});
});
According to what I've read online on various GitHub issue threads, it seems like a common approach is to make console.warn/console.error throw.
So when you are writing your tests you can do something like
expect(// Render Component //).to.throw();
Hope this helps.
More info: https://github.com/airbnb/enzyme/issues/588
I am trying test my connected component of my React/Redux app and I wrote some test case which actually throws the error:
App component › shows account info and debits and credits`
Invariant Violation: Could not find "store" in either the context or props of "Connect(AccountInfo)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(AccountInfo)".
The test case which trow an error app.test.js is below. And my problem is that I don't understand what should I wrap here by Connect() because I didn't use AccountInfo here:
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import App from './App';
import * as actions from '../../actions';
function setup() {
const props = {
errorMessage: null,
actions
};
const enzymeWrapper = mount(<App {...props} />);
return {
props,
enzymeWrapper,
};
}
describe('App component', () => {
it('shows account info and debits and credits`', () => {
const {enzymeWrapper} = setup();
expect(enzymeWrapper.find('.account-info').exists()).toBe(true);
expect(enzymeWrapper.find('.debits-and-credits').exists()).toBe(true);
});
it('shows error message', () => {
const {enzymeWrapper} = setup();
enzymeWrapper.setProps({ errorMessage: 'Service Unavailable' });
expect(enzymeWrapper.find('.error-message').exists()).toBe(true);
});
});
My containers/app.js:
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions';
import AppComponent from '../components/App/App';
const mapStateToProps = state => ({
isFetching: state.balance.isFetching,
errorMessage: state.errorMessage,
});
const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(actions, dispatch),
});
const AppContainer = connect(mapStateToProps, mapDispatchToProps)(AppComponent);
export default AppContainer;
The component app.js:
import React, { Component } from 'react';
import ErrorMessage from '../../containers/ErrorMessage';
import AccountInfo from '../../containers/AccountInfo';
import DebitsAndCredits from '../../containers/DebitsAndCredits';
import './App.css';
const AppComponent = () =>
<div className="app">
<AccountInfo />
<DebitsAndCredits />
</div>;
export class App extends Component {
componentWillMount() {
const { actions } = this.props;
actions.fetchBalance();
}
render() {
const { errorMessage } = this.props;
return errorMessage ? <ErrorMessage /> : <AppComponent />;
}
}
export default App;
UPD:
I updated my test case and now it looks like:
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import createSagaMiddleware from 'redux-saga';
import { initialState } from '../../reducers/balance/balance';
import App from './App';
import * as actions from '../../actions';
const middlewares = [createSagaMiddleware];
const mockStore = configureMockStore(middlewares);
const store = mockStore(initialState);
function setup() {
const props = {
errorMessage: null,
actions,
};
const enzymeWrapper = mount(
<Provider store={store}>
<App {...props} />
</Provider>
);
return {
props,
enzymeWrapper,
};
}
describe('App component', () => {
it('shows account info and debits and credits`', () => {
const { enzymeWrapper } = setup();
expect(enzymeWrapper.find('.account-info').exists()).toBe(true);
expect(enzymeWrapper.find('.debits-and-credits').exists()).toBe(true);
});
it('shows error message', () => {
const { enzymeWrapper } = setup();
enzymeWrapper.setProps({ errorMessage: 'Service Unavailable' });
expect(enzymeWrapper.find('.error-message').exists()).toBe(true);
});
});
And my error now is:
App component › shows account info and debits and credits`
TypeError: Cannot read property 'account' of undefined
UPD 2:
My initialState which I put when I create mocked store:
const initialState = {
isFetching: false,
account: {},
currency: '',
debitsAndCredits: [],
};
My AccountInfo component:
import React from 'react';
const AccountInfo = ({ account, currency }) =>
<header className="account-info">
<p>{account.name}</p>
<p>
IBAN: {account.iban}<br />
Balance: {account.balance}<br />
Currency: {currency}<br />
</p>
</header>;
export default AccountInfo;
For testing the connected component, you need to mock the provider as well, since the connect picks state variables from redux store.
Do this
const enzymeWrapper = mount (<Provider store={mockStore}><App {...props}/></Provider>)
You need to mock the redux store too.
Edit 1:
Just looking at your AccountInfo component it tells me that you are expecting account in the props here.
AccountInfo = ({account}) =>
So that means App.js has to pass down the accounts' value in the props. Same thing goes for currency.