Test React Component using Redux and react-testing-library - javascript

I am new to testing redux connected components in React and trying to figure out how to test them.
Currently I'm using react-testing-library and having trouble setting up the my renderWithRedux function to correctly setup redux.
Here is a sample component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
class Sample extends Component {
constructor(props) {
super(props);
this.state = {
...
}
}
componentDidMount() {
//do stuff
console.log(this.props)
}
render() {
const { user } = this.props
return(
<div className="sample">
{user.name}
</div>
)
}
}
const mapStateToProps = state => ({
user: state.user
})
export default connect(mapStateToProps, {})(Sample);
Here is a sample test:
import React from 'react';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { render, cleanup } from 'react-testing-library';
import Sample from '../components/sample/'
const user = {
id: 1,
name: "John Smith"
}}
function reducer(state = user, action) {
//dont need any actions at the moment
switch (action.type) {
default:
return state
}
}
function renderWithRedux(
ui,
{ initialState, store = createStore(reducer, initialState) } = {}
) {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
}
}
afterEach(cleanup)
test('<Sample> example text', () => {
const { getByTestId, getByLabelText } = renderWithRedux(<Sample />)
expect(getByText(user.name))
})
The user prop value always results as undefined. I have re-written this a couple of ways but can't seem to get it to work. If I pass the user data directly as a prop to Sample component in the test, it still resolves to be undefined.
I am learning from the tutorials and examples via the offical docs, like this one: https://github.com/kentcdodds/react-testing-library/blob/master/examples/tests/react-redux.js
Any pointers, tips or solutions would be greatly appreciated!

You should wrap the component inside Provider, here is the simple example
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom';
import { Provider } from "react-redux";
import configureMockStore from "redux-mock-store";
import TestedComponent from '../index';
const mockStore = configureMockStore();
const store = mockStore({});
const renderTestedComponent = () => {
return render(
<Provider store={store}>
<TestedComponent />
</Provider>
);
};
describe('test TestedComponent components', () => {
it('should be render the component correctly', () => {
const { container } = renderTestedComponent();
expect(container).toBeInTheDocument();
});
});

**Unable to Fire event using #testing-library**
// demo.test.js
import React from 'react'
import { Provider } from "react-redux";
import '#testing-library/react/cleanup-after-each'
import '#testing-library/jest-dom/extend-expect'
import { render, fireEvent } from '#testing-library/react'
// this is used to fire the event
// import userEvent from "#testing-library/user-event";
//import 'jest-localstorage-mock';
import ChangePassword from './ChangePassword';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
test('test 1-> Update User password', () => {
// global store
const getState = {
authUser :{
user : {
email: "test#gmail.com",
id: 0,
imageURL: null,
name: "test Solutions",
roleId: 1,
roleName: "testRole",
userName: "testUserName"
},
loading: false,
showErrorMessage: false,
errorDescription: ""
}
}; // initial state of the store
// const action = { type: 'LOGIN_USER' };
// const expectedActions = [action];
// const store = mockStore(getState, expectedActions);
const onSaveChanges = jest.fn();
const changePassword = jest.fn();
const store = mockStore(getState);
const { queryByText, getByLabelText, getByText , getByTestId , getByPlaceholderText, } = render(
<Provider store={store}>
<ChangePassword
onSaveChanges={onSaveChanges}
changePassword={changePassword}
/>
</Provider>,
)
// test 1. check the title of component
expect(getByTestId('updateTitle')).toHaveTextContent('Update your password');
// test 2. chekck the inputfile
expect(getByPlaceholderText('Old Password')) //oldpassword
expect(getByPlaceholderText('New Password')) //newpassword
expect(getByPlaceholderText('Confirm New Password')) //confpassword
// change the input values
fireEvent.change(getByPlaceholderText("Old Password"), {
target: { value: "theOldPasword" }
});
fireEvent.change(getByPlaceholderText("New Password"), {
target: { value: "#Ab123456" }
});
fireEvent.change(getByPlaceholderText("Confirm New Password"), {
target: { value: "#Ab123456" }
});
// check the changed input values
expect(getByPlaceholderText('Old Password').value).toEqual("theOldPasword");
expect(getByPlaceholderText('New Password').value).toEqual("#Ab123456");
expect(getByPlaceholderText('Confirm New Password').value).toEqual("#Ab123456");
expect(getByText('Save Changes')); // check the save change button
// calling onSave function
//fireEvent.click(getByTestId('savechange'))
// userEvent.click(getByText('Save Changes'));
})

Related

React Testing Library with Redux - Mocking Axios in action creator

I have an app I'm adding integration tests to for learning React Testing Library.
It's built in MERN stack, along with Redux for state management.
My test wrapper is a standard setup:
import React from 'react';
import { render as rtlRender } from '#testing-library/react';
import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const render = (
ui,
{
initialState,
store = createStore(rootReducer, compose(applyMiddleware(thunk))),
...renderOptions
} = {}
) => {
const Wrapper = ({ children }) => {
return <Provider store={store}>{children}</Provider>;
};
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
};
export * from '#testing-library/react';
export { render };
At the moment I'm trying to test a login form and errors that are returned when fields aren't valid.
import React from 'react';
import { Router } from 'react-router';
import '#testing-library/jest-dom';
import { createMemoryHistory } from 'history';
import { render, screen, fireEvent, waitFor } from '../../../utils/test-utils';
import Login from '../login';
jest.mock('axios', () => {
return {
post: jest.fn()
};
});
describe('<Login/>', () => {
beforeEach(() => {
// Some requirements for the component to render in the test
const history = createMemoryHistory();
const state = '';
history.push('/', state);
render(
<Router history={history}>
<Login history={history} />
</Router>
);
});
test('should show empty email error', async () => {
fireEvent.input(screen.getByRole('textbox', { name: /email/i }), {
target: {
value: ''
}
});
fireEvent.submit(screen.getByRole('button', { name: /login/i }));
await waitFor(() => {
expect(screen.getByText(/email field is required/i)).toBeInTheDocument();
});
});
});
Unfortunately, when I run this test it gives me TypeError: Cannot read property 'then' of undefined and I can't figure out why
My action looks like:
export const loginUser = (userData) => (dispatch) => {
return axios
.post('/api/users/login', userData)
.then((res) => {
const { token } = res.data;
localStorage.setItem('jwtToken', token);
setAuthToken(token);
const decoded = jwt_decode(token);
dispatch(setCurrentUser(decoded));
})
.catch((err) =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
);
};
Instead of testing the actions/reducers in my codebase I'd rather test what the user should see, which is what I've read Testing Library encourages.
I'm also using redux hooks - useDispatch/useSelector, throughout my app.
Any help would be grateful :)

Redux - Dispatching Action (onClick Event)

I am simply trying to connect() my LoginPage (component) to my Redux Store and dispatch in action via a onClick (event). When I console.log(this.props) my dispatch handler login() isn't in the component's props.
GitHub Repo -- https://github.com/jdavis-software/demo.git
Question: Why isn't my Redux Store either connection or dispatching the actions?
LoginPage:
import React, { Component} from 'react';
import { connect } from 'react-redux';
export class LoginPage extends Component<any> {
render(){
console.log('props doesnt have contain - login(): ', this.props)
return (<button onClick={ () => '' }>Login</button>)
}
}
const mapProps = state => ({ user: state.user })
const dispatchProps = (dispatch) => {
return {
login: () => dispatch({ type: 'USER_LOGGED_IN', payload: true})
}
}
export default connect(mapProps,dispatchProps)(LoginPage)
Redux Configuration:
import { IStore, IUser } from '#interfaces';
import { createStore, combineReducers } from 'redux';
import ReduxPromise from 'redux-promise';
// reducers
import userReducer from './user.reducer';
// define the intial global store state
const initialState:IStore = {
user: {
isAuthenticated: false
}
}
const appReducer = combineReducers({user: userReducer})
export default createStore(appReducer,initialState);
User Reducer:
// initial state
const initalState:IUser = {
isAuthenticated: false
}
// reducer
const userReducer = (state:IUser = initalState, { type, payload}: IPayload): IUser => {
console.log('user reducer start', state)
switch (type) {
case 'USER_LOGGED_IN':
state = { ...state, isAuthenticated: payload }
break;
default:
return state;
}
return state;
};
export default userReducer;
Root Page:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
// styles
import './index.scss';
// pages
import { App } from '#pages';
// store
import store from './core/store/store';
render(
<Provider store={store}>
<App/>
</Provider>, document.getElementById('app')
);
I checked your code on git repository. I found out that you're exporting the named export
export class LoginPage
and the default export,
export default connect(mapProps,dispatchProps)(LoginPage)
But when you're accessing it, you're accessing it as
import { /*Other components*/ , LoginPage } from '#pages'
So it is actually taking the named exported component which is not connected to store.
I suggest you to import as
import LoginPage , { /*Other components*/ } from '#pages'
This might solve your problem.
Return statements are missing in the properties of connect.
const mapProps = state => { return {user: state.user} }
const dispatchProps = (dispatch) => {
return {
login: () => dispatch({ type: 'USER_LOGGED_IN', payload: true})
}
}
export default connect(mapProps,dispatchProps)(LoginPage)
Updated:
Please check Redux-dispatch
try:
import React, { Component} from 'react';
import { connect } from 'react-redux';
export class LoginPage extends Component<any> {
render(){
console.log('props doesnt contain - login(): ', this.props)
return (
<button onClick={ this.props.login }>Login</button>
)
}
}
const mapProps = state => ({ user: state.user })
const dispatchProps = (dispatch) => ({
login: () => dispatch({ type: 'USER_LOGGED_IN', payload: true})
})
export default connect(mapProps,dispatchProps)(LoginPage)
to return an object with Arrow Functions you need to wrap your {} with ()

How do i access redux state from another react component?

I am developing a lottery statistics app that gets data from a csv loaded from an input then I was wanting to read this data to the redux store so I can use it across multiple components.
I have successfully saved the data to the redux store once I import the file and read it through Header.js and using an action, but I am not sure how to access this in other components like e.g. Main.js.
I feel like I am still confused on how react/redux all fits together. I'm sorry if this has been asked before but everything I looked up online I couldn't get to work.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import reducers from "./reducers";
import App from "./components/App";
const store = createStore(reducers, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);
// App.js
import React from "react";
import Header from "./Header";
import Main from "./Main";
const App = () => {
return (
<div>
<Header />
<Main />
<div className="numbers-for-draw"></div>
</div>
);
};
export default App;
// Header.js
import React from "react";
import { CSVReader } from "react-papaparse";
import { fetchData } from "../actions";
import { connect } from "react-redux";
class Header extends React.Component {
constructor(props) {
super(props);
this.fileInput = React.createRef();
}
handleReadCSV = data => {
this.props.fetchData(data);
console.log(this.props.data);
};
handleOnError = (err, file, inputElem, reason) => {
console.log(err);
};
handleImportOffer = () => {
this.fileInput.current.click();
console.log("Got to handleImportOffer");
};
render() {
return (
<header>
<CSVReader
onFileLoaded={this.handleReadCSV}
inputRef={this.fileInput}
style={{ display: "none" }}
onError={this.handleOnError}
/>
<button onClick={this.handleImportOffer}>Import</button>
</header>
);
}
}
//Map what is in the redux store (e.g. state) to props
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {
fetchData: fetchData
})(Header);
// Main.js
import React from "react";
import { fetchData } from "../actions";
import { connect } from "react-redux";
const Main = () => {
console.log("In main");
console.log(this.props.data); //Blows up here.
return <div>Main</div>;
};
//Map what is in the redux store (e.g. state) to props
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {
fetchData: fetchData
})(Main);
// actions/index.js
export const fetchData = data => dispatch => {
console.log("Action");
const lottoData = {
stringNumbers: [
"one",
"two",
"three",
...
],
allResults: [],
winningNumbers: [],
winningNumbersAsStrings: []
};
const localData = data.data;
localData.shift();
localData.forEach(line => {
const lineObject = {
draw: line[0],
drawDate: line[1],
ballOne: line[2],
ballTwo: line[3],
ballThree: line[4],
ballFour: line[5],
ballFive: line[6],
ballSix: line[7],
bonusBall: line[8],
bonusBall2: line[9],
powerBall: line[10]
};
lottoData.allResults.push(lineObject);
let nums = [];
nums.push(parseInt(line[2]));
nums.push(parseInt(line[3]));
nums.push(parseInt(line[4]));
nums.push(parseInt(line[5]));
nums.push(parseInt(line[6]));
nums.push(parseInt(line[7]));
nums.sort((a, b) => {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
});
lottoData.winningNumbers.push(nums);
lottoData.winningNumbersAsStrings.push(nums.toString());
});
dispatch({ type: "FETCH_DATA", payload: lottoData });
};
// lottoReducer.js
export default (state = {}, action) => {
switch (action.type) {
case "FETCH_DATA":
return action.payload;
default:
return state;
}
};
// reducers/index.js
import { combineReducers } from "redux";
import lottoReducer from "./lottoReducer";
export default combineReducers({
data: lottoReducer
});
I haven't tested your code, but it seems to me that the only problem is in your Main.js
While you use a function component and not a class, you shouldn't use this to access your props. The following should work as expected:
const Main = (props) => {
console.log("In main");
console.log(props.data);
return <div>Main</div>;
};
//Map what is in the redux store (e.g. state) to props
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {
fetchData: fetchData
})(Main);
In your main.js you used functional components so this.props doesn't work there. You must pass props to your component and console.log(props.data).

Test connected component in React/Redux

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.

React Redux - store updates, component re-renders... but not with new data

I have a complicated App with a problem which I have reduced down to 3 files below. Essentially, what is happening is:
component loads and render initial text
a spoof API is then triggered which calls this.props.route.onLoadData()
this in turn calls a reducer which returns a new object containing updated text
the component re-renders accordingly, but not with the updated text
I'm using <Provider/> and connect() - neither succeeds.
Any ideas?
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, hashHistory } from 'react-router'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import Reducers from './reducers'
import Problem from './Problem'
var store = createStore(Reducers)
var routes = [
{ path: '/problem',
component: Problem,
data: store.getState(),
onLoadData: (res) => store.dispatch({ type: 'LOAD_DATA', data: res })
}
]
var render = function() {
return ReactDOM.render(
<Provider store={store}>
<Router history={hashHistory} routes={routes}/>
</Provider>,
document.getElementById('app')
)
}
render()
store.subscribe(render)
problem.js
import React from 'react'
import { connect } from 'react-redux'
var ProblemContainer = React.createClass({
spoofAPI() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ text: 'updated text' })
}, 1000)
})
},
componentDidMount() {
this.spoofAPI().then((res) => {
return this.props.route.onLoadData( res )
}, console.error)
},
render() {
var text = this.props.route.data.text
console.log('render:', text)
return (
<div>{text}</div>
)
}
})
const mapState = state => ({ text: state })
export default connect(mapState)(ProblemContainer)
reducers.js
export default (state = { text: 'initial text' }, action) => {
switch (action.type) {
case 'LOAD_DATA':
var newState = Object.assign({}, state)
console.log('reducer - existing state: ', newState.text)
newState.text = action.data.text
console.log('reducer - receives: ', action.data.text)
var returnObject = Object.assign({}, state, newState)
console.log('reducer - returnObject: ', returnObject)
return returnObject
default:
return state
}
}
According to these two pieces of code:
var routes = [
...
data: initialState,
...
]
and
render() {
var text = this.props.route.data.text
...
}
It looks like what you are doing is always referencing one and the same piece of data (which is initialState) without even touching store.
UPDATE. What you have to do is to connect your Component to store via react-redux and use store values in your Component.
import React from 'react'
import { connect } from 'react-redux'
const Foo = React.createClass({
...
render() {
var text = this.props.text
console.log('render:', text)
return (
<div>{text}</div>
)
}
})
const mapState = state => ({ text: state });
export default connect(mapState)(Foo);
Adding an onGetState() which can be called from the component works... but is this an anti-pattern of sorts (?)
index.js
var routes = [
{ ...
onGetState: () => store.getState()
...
}
]
problem.js
render() {
var { text } = this.props.route.onGetState()
...
}

Categories

Resources