I want to test my pure component so I'm doing this:
MyComp.js
export MyComp = props => {
return (
<Wrapper>Content</Wrapper>
)
}
const MyCompConn = connect()(MyComp);
export default MyCompConn;
Where <Wrapper>:
export Wrapper = ({children}) => {
return (
<div>{children}</div>
)
}
const WrapperConn = connect()(Wrapper);
export default WrapperConn;
MyComp.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import { MyComp } from '../../MyComp';
describe('With Snapshot Testing', () => {
it('renders!"', () => {
const component = renderer.create(<Login />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
Now, when I run yarn test I get this error:
Invariant Violation: Could not find "store" in either the context or props of "Connect(AppWrapper)". Either wrap the root component in a <Provider> or explicitly pass "store" as a prop to "Connect(AppWrapper)"
And this is happening because <Wrapper> is connected in my <MyComp> component, but I'm not sure how to test just the latter even if it's wrapped on a connected component.
To test our component without using mocked store, we can mock the connect of react-redux itself using Jest. PFB the example:
import React from 'react';
import renderer from 'react-test-renderer';
import { MyComp } from '../../MyComp';
jest.mock('react-redux', () => ({
connect: () => {
return (component) => {
return component
};
}
}));
describe('With Snapshot Testing', () => {
it('renders!"', () => {
const component = renderer.create(<Login />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
Now, this will render the Wrapper component directly, and not the connected Wrapper component.
Try this:
import configureMockStore from 'redux-mock-store';
const store = mockStore({});
const component = renderer.create(<Provider store={store}><Login /></Provider>);
Related
Considering the following project setup on a react-redux application that uses context API to avoid prop drilling. The example given is simplified.
Project Setup
React project uses React Redux
Uses context API to avoid prop drilling in certain cases.
Redux store has a prop posts which contains list of posts
An action creator deletePost(), which deletes a certain post by post id.
To avoid prop drilling, both posts and deletePosts() is added to a context AppContext and returned by a hook funciton useApp().
posts array is passed via contexts so it is not used by connect() function. Important
Problem:
When action is dispatched store is updated however Component is not re-rendered (because the prop is not connected?). Of course, if I pass the prop with connect function and drill it down to child rendering works fine.
What is the solution?
Example Project
The example project can be found in codesandbox. Open up the console and try to click the delete button. You will see no change in the UI while you can see the state is updated in the console.
Codes
App.js
import Home from "./routes/Home";
import "./styles.css";
import { AppProvider } from "./context";
export default function App() {
return (
<AppProvider>
<div className="App">
<Home />
</div>
</AppProvider>
);
}
context.js
import { useDispatch, useStore } from "react-redux";
import { useContext, createContext } from "react";
import { deletePost } from "./redux/actions/posts";
export const AppContext = createContext();
export const useApp = () => {
return useContext(AppContext);
};
export const AppProvider = ({ children }) => {
const dispatch = useDispatch();
const {
posts: { items: posts }
} = useStore().getState();
const value = {
// props
posts,
// actions
deletePost,
dispatch
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
Home.js
import { connect } from "react-redux";
import Post from "../components/Post";
import { useApp } from "../context";
const Home = () => {
const { posts } = useApp();
return (
<section>
{posts.map((p) => (
<Post key={p.id} {...p} />
))}
</section>
);
};
/*
const mapProps = ({ posts: { items: posts } }) => {
return {
posts
};
};
*/
export default connect()(Home);
Post.js
import { useApp } from "../context";
const Post = ({ title, content, id }) => {
const { deletePost, dispatch } = useApp();
const onDeleteClick = () => {
console.log("delete it", id);
dispatch(deletePost(id));
};
return (
<article>
<h1>{title}</h1>
<p>{content}</p>
<div className="toolbar">
<button onClick={onDeleteClick}>Delete</button>
</div>
</article>
);
};
export default Post;
You're not using the connect higher order component method properly . Try using it like this so your component will get the states and the function of your redux store :
import React from 'react';
import { connect } from 'react-redux';
import { callAction } from '../redux/actions.js';
const Home = (props) => {
return (
<div> {JSON.stringify(props)} </div>
)
}
const mapState = (state) => {
name : state.name // name is in intialState
}
const mapDispatch = (dispatch) => {
callAction : () => dispatch(callAction()) // callAction is a redux action
//and should be imported in the component also
}
export default connect(mapState , mapDispatch)(Home);
You can access the states and the actions from your redux store via component props.
Use useSelector() instead of useState(). Example codepen is fixed.
Change from:
const { posts: { items: posts } } = useStore().getState();
Change to:
const posts = useSelector(state => state.posts.items);
useStore() value is only received when component is first mounted. While useSlector() will get value when value is changed.
I am really new in Hooks and during learning faces many difficulties to switch from the old style.
My old code looks like:
context.js
import React from "react";
const SomeContext = React.createContext(null);
export const withContext = (Component) => (props) => (
<SomeContext.Consumer>
{(server) => <Component {...props} server={server} />}
</SomeContext.Consumer>
);
export default SomeContext;
main index.js
<SomeContext.Provider value={new SomeClass()}>
<App />
</SomeContext.Provider>
but when I want to access it through with export default withContext(SomeComponent) by this.props.server.someFunc() it showed props is undefined in the classless hook function.
how can I achieve this.props in react hook
Edit:
SomeClass is not React inheriting class and its look like it.
class SomeClass {
someFunc = (id) => axios('api endpoints')
}
SomeComponent
const SomeComponent = () => {
...
useEffect(() => {
this.props.server.someFunc(id).then(...).catch(...)
}, ...)
...
}
In the regular case, you need to export the Context, then import it and use it within useContext:
export const SomeContext = React.createContext(null);
// Usage
import { SomeContext } from '..';
const SomeComponent = () => {
const server = useContext(SomeContext);
useEffect(() => {
server.someFunc(id);
});
};
But in your case, since you using HOC, you have the server within the prop it self:
const SomeComponent = (props) => {
useEffect(() => {
props.server.someFunc(id);
});
};
I'm having issues testing my components that use dispatch via useReducer with React-testing-library.
I created a less complex example to try to boil down what is going on and that is still having the same dispatch is not a function problem. When I run my tests, I am getting this error:
11 | data-testid="jared-test-button"
12 | onClick={() => {
> 13 | dispatch({ type: 'SWITCH' })
| ^
14 | }}
15 | >
16 | Click Me
Also, if I do a console.log(typeof dispatch) inside RandomButton, and I click on the button the output says function.
Here is the test in question.
import React from 'react'
import RandomButton from '../RandomButton'
import { render, fireEvent } from '#testing-library/react'
describe('Button Render', () => {
it('click button', () => {
const { getByTestId, queryByText } = render(<RandomButton />)
expect(getByTestId('jared-test-button')).toBeInTheDocument()
fireEvent.click(getByTestId('jared-test-button'))
expect(queryByText('My name is frog')).toBeInTheDocument()
})
})
Here is my relevant code:
RandomButton.js
import React, { useContext } from 'react'
import MyContext from 'contexts/MyContext'
const RandomButton = () => {
const { dispatch } = useContext(MyContext)
return (
<div>
<Button
data-testid="jared-test-button"
onClick={() => {
dispatch({ type: 'SWITCH' })
}}
>
Click Me
</Button>
</div>
)
}
export default RandomButton
MyApp.js
import React, { useReducer } from 'react'
import {myreducer} from './MyFunctions'
import MyContext from 'contexts/MyContext'
import RandomButton from './RandomButton'
const initialState = {
blue: false,
}
const [{ blue },dispatch] = useReducer(myreducer, initialState)
return (
<MyContext.Provider value={{ dispatch }}>
<div>
{blue && <div>My name is frog</div>}
<RandomButton />
</div>
</MyContext.Provider>
)
export default MyApp
MyFunctions.js
export const myreducer = (state, action) => {
switch (action.type) {
case 'SWITCH':
return { ...state, blue: !state.blue }
default:
return state
}
}
MyContext.js
import React from 'react'
const MyContext = React.createContext({})
export default MyContext
It is probably something stupid that I am missing, but after reading the docs and looking at other examples online I'm not seeing the solution.
I've not tested redux hooks with react-testing-library, but I do know you'll have to provide a wrapper to the render function that provides the Provider with dispatch function.
Here's an example I use to test components connected to a redux store:
testUtils.js
import React from 'react';
import { createStore } from 'redux';
import { render } from '#testing-library/react';
import { Provider } from 'react-redux';
import reducer from '../reducers';
// https://testing-library.com/docs/example-react-redux
export const renderWithRedux = (
ui,
{ initialState, store = createStore(reducer, initialState) } = {},
options,
) => ({
...render(<Provider store={store}>{ui}</Provider>, options),
store,
});
So, based upon what you've shared I think the wrapper you'd want would look something like this:
import React from 'react';
import MyContext from 'contexts/MyContext';
// export so you can test that it was called with specific arguments
export dispatchMock = jest.fn();
export ProviderWrapper = ({ children }) => (
// place your mock dispatch function in the provider
<MyContext.Provider value={{ dispatch: dispatchMock }}>
{children}
</MyContext.Provider>
);
and in your test:
import React from 'react';
import RandomButton from '../RandomButton';
import { render, fireEvent } from '#testing-library/react';
import { ProviderWrapper, dispatchMock } from './testUtils';
describe('Button Render', () => {
it('click button', () => {
const { getByTestId, queryByText } = render(
<RandomButton />,
{ wrapper: ProviderWrapper }, // Specify your wrapper here
);
expect(getByTestId('jared-test-button')).toBeInTheDocument();
fireEvent.click(getByTestId('jared-test-button'));
// expect(queryByText('My name is frog')).toBeInTheDocument(); // won't work since this text is part of the parent component
// If you wanted to test that the dispatch was called correctly
expect(dispatchMock).toHaveBeenCalledWith({ type: 'SWITCH' });
})
})
Like I said, I've not had to specifically test redux hooks but I believe this should get you to a good place.
I am trying to test my component which is consuming data from context via HOC.
Here is setup:
Mocked context module /context/__mocks__
const context = { navOpen: false, toggleNav: jest.fn() }
export const AppContext = ({
Consumer(props) {
return props.children(context)
}
})
Higher OrderComponent /context/withAppContext
import React from 'react'
import { AppContext } from './AppContext.js'
/**
* HOC with Context Consumer
* #param {Component} Component
*/
const withAppContext = (Component) => (props) => (
<AppContext.Consumer>
{state => <Component {...props} {...state}/>}
</AppContext.Consumer>
)
export default withAppContext
Component NavToggle
import React from 'react'
import withAppContext from '../../../context/withAppContext'
import css from './navToggle/navToggle.scss'
const NavToggle = ({ toggleNav, navOpen }) => (
<div className={[css.navBtn, navOpen ? css.active : null].join(' ')} onClick={toggleNav}>
<span />
<span />
<span />
</div>
)
export default withAppContext(NavToggle)
And finally Test suite /navToggle/navToggle.test
import React from 'react'
import { mount } from 'enzyme'
beforeEach(() => {
jest.resetModules()
})
jest.mock('../../../../context/AppContext')
describe('<NavToggle/>', () => {
it('Matches snapshot with default context', () => {
const NavToggle = require('../NavToggle')
const component = mount( <NavToggle/> )
expect(component).toMatchSnapshot()
})
})
Test is just to get going, but I am facing this error:
Warning: Failed prop type: Component must be a valid element type!
in WrapperComponent
Which I believe is problem with HOC, should I mock that somehow instead of the AppContext, because technically AppContext is not called directly by NavToggle component but is called in wrapping component.
Thanks in advance for any input.
So I solved it.
There were few issues with my attempt above.
require does not understand default export unless you specify it
mounting blank component returned error
mocking AppContext with __mock__ file caused problem when I wanted to modify context for test
I have solved it following way.
I created helper function mocking AppContext with custom context as parameter
export const defaultContext = { navOpen: false, toggleNav: jest.fn(), closeNav: jest.fn(), path: '/' }
const setMockAppContext = (context = defaultContext) => {
return jest.doMock('../context/AppContext', () => ({
AppContext: {
Consumer: (props) => props.children(context)
}
}))
}
export default setMockAppContext
And then test file ended looking like this
import React from 'react'
import { shallow } from 'enzyme'
import NavToggle from '../NavToggle'
import setMockAppContext, { defaultContext } from '../../../../testUtils/setMockAppContext'
beforeEach(() => {
jest.resetModules()
})
describe('<NavToggle/>', () => {
//...
it('Should have active class if context.navOpen is true', () => {
setMockAppContext({...defaultContext, navOpen: true})
const NavToggle = require('../NavToggle').default //here needed to specify default export
const component = shallow(<NavToggle/>)
expect(component.dive().dive().hasClass('active')).toBe(true) //while shallow, I needed to dive deeper in component because of wrapping HOC
})
//...
})
Another approach would be to export the component twice, once as decorated with HOC and once as clean component and create test on it, just testing behavior with different props. And then test just HOC as unit that it actually passes correct props to any wrapped component.
I wanted to avoid this solution because I didn't want to modify project file(even if it's just one word) just to accommodate the tests.
I have a react component with Redux #connect decorator, for instance:
import React, { Component } from 'react'
import { connect } from 'react-redux'
#connect(mapStateToProps,
{
onPress: () => {...code}) // Component receives this func, not passed from the test
}
export class Component extends Component {
render () {
return <button onclick={this.props.onPress>....</button>
}
}
I faced with a problem that mocked functions passed to the component in a test file are not passed into the component:
const store = createStore(
combineReducers({name: reducer})
);
const ComponentConnected = connect(..., {
onPress: {jest.fn()} // Component doesn't receive this mock
})(() =>(<Component />));
describe('Component testing', () => {
it('should render component', () => {
const wrapper = mount(
<Provider store={store}>
<ComponentConnected />
</Provider>
);
expect(wrapper.find(Component)).toHaveLength(1);
});
});
Also, I tried to pass the mock function as a tested component prop, it didn't help too. Is it possible to solve this problem without re-writing component #connect decorator?
I found out how to pass a prop to a connected Component. I used shallow, dive and setProps enzyme methods:
describe('Component testing', () => {
it('should render component', () => {
const wrapper = shallow(
<Provider store={store}>
<ComponentConnected />
</Provider>
);
const connectedComponentWrapper = wrapper.dive().dive();
connectedComponentWrapper.setProps({anyProp: 'anyProp'});
});
});
I think, instead of mocking connect function of redux-connect. You should mock the action itself. replace ../actions with your actions file.
import { onPress } from "../actions";
jest.mock("../actions");
onPress.mockReturnValue({ someval: 1 });