How to mock an async action creator with jest - unit-testing

I am learning unit/integration testing with Jest and Enzyme and getting stuck on how to mock an async action creator.
To be more accurate, my goal is to write a test to check if fetchComments() is called when the app is mount in the App.tsx file below (irrelevant code is omitted, see the whole code here).
export default function App() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchDishes());
dispatch(fetchLeaders());
dispatch(fetchComments());
dispatch(fetchPromotions());
}, []);
return (
<div>
<Header />
<Switch>
<Route exact path="/">
<Home />
</Route>
{/* code omitted */}
<Redirect to="/" />
</Switch>
<Footer />
</div>
)
}
I've done the following steps to achieve the goal:
Create a fake Redux store with middleware for testing as shown below. You also can see the real Redux store implementation here.
export const storeFactory = (initialState: AppState = defaultAppState) => {
return createStore(appReducer, initialState, applyMiddleware(...middlewares));
}
Mock fetchComments() action creator. The real implementation of the action creator is in /src/redux/comment/commentActions.ts file, and the mock implementation is in /src/redux/comment/mocks/commentActions.ts file. And here is how I mock it:
module.exports = {
...jest.requireActual('../commentActions.ts'),
__esModule: true,
fetchComments: jest.fn().mockReturnValue({
type: INIT_COMMENTS,
payload: []
})
}
And finally write test in ```App.test.tsx`` file
jest.mock('./redux/comment/commentActions');
const fetchComments: jest.Mock = require('./redux/comment/commentActions').fetchComments;
const setup = () => {
const store = storeFactory();
return mount(
<Provider store={ store }>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
)
};
describe('fetch data on app mount', () => {
beforeEach(() => {
fetchComments.mockClear();
});
test('should fetch comments on app mount', () => {
const wrapper = setup();
expect(fetchComments).toBeCalledTimes(1);
})
});
But I got the following error message:
Actions must be plain objects. Use custom middleware for async actions.
24 | dispatch(fetchDishes());
25 | dispatch(fetchLeaders());
> 26 | dispatch(fetchComments());
| ^
27 | dispatch(fetchPromotions());
28 | }, []);
29 |
at dispatch (node_modules/redux/lib/redux.js:206:13)
at dispatch (node_modules/redux-thunk/lib/index.js:14:16)
at App (src/App.tsx:26:5)
at commitHookEffectListMount (node_modules/react-dom/cjs/react-dom.development.js:19731:26)
at commitPassiveHookEffects (node_modules/react-dom/cjs/react-dom.development.js:19769:11)
at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:188:14)
at HTMLUnknownElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:318:25)
at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:94:17)
at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:231:34)
at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:237:16)
at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:292:31)
at flushPassiveEffectsImpl (node_modules/react-dom/cjs/react-dom.development.js:22853:9)
at unstable_runWithPriority (node_modules/scheduler/cjs/scheduler.development.js:653:12)
at runWithPriority$1 (node_modules/react-dom/cjs/react-dom.development.js:11039:10)
at flushPassiveEffects (node_modules/react-dom/cjs/react-dom.development.js:22820:12)
at performSyncWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:21737:3)
at node_modules/react-dom/cjs/react-dom.development.js:11089:24
at unstable_runWithPriority (node_modules/scheduler/cjs/scheduler.development.js:653:12)
at runWithPriority$1 (node_modules/react-dom/cjs/react-dom.development.js:11039:10)
at flushSyncCallbackQueueImpl (node_modules/react-dom/cjs/react-dom.development.js:11084:7)
at flushSyncCallbackQueue (node_modules/react-dom/cjs/react-dom.development.js:11072:3)
at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:21862:7)
at Object.act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:929:14)
at wrapAct (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:405:13)
at Object.render (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:474:16)
at new ReactWrapper (node_modules/enzyme/src/ReactWrapper.js:115:16)
at mount (node_modules/enzyme/src/mount.js:10:10)
at setup (src/App.test.tsx:14:12)
at Object.test (src/App.test.tsx:36:25)
I searched the error message on StackOverflow and all answers suggest that I should add middleware to the Redux store. But I already did that! So I think there might be some hidden mistakes in my code. I hope that someone can help me point it out.

Related

Tenary Operator outputs nextjs component when condition is false and it should not show

On my Static next.js website, I have a component <GoogleAnalytics /> holding my Google Analytics snippet.
I want this snippet to not be called on my localhost:3000
In my _app.js I have the following Tenary Operator but it is still showing the code of the component on localhost:3000.
import { useState, useEffect } from "react";
import GoogleAnalytics from "../components/GoogleAnalytics";
function MyApp({ Component, pageProps }) {
const [host, setHost] = useState();
useEffect(() => {
setHost(window.location.host);
}, []);
return (
<>
{host === "localhost:3000" ? <></> : <GoogleAnalytics />}
<Component {...pageProps} />
</>
);
}
export default MyApp;
Any idea what I'm doing wrong?

Vue.js unit test - mock service with async data

Project: https://github.com/marioedgar/webpack-unit-test
I have a Vue.js app I generated with the vue CLI. I've only edited the HelloWorld component slightly to fetch some async data from my test service, you can see that here:
<template>
<h1>{{ message }}</h1>
</template>
<script>
import service from './test.service'
export default {
name: 'HelloWorld',
created () {
service.getMessage().then(message => {
this.message = message
})
},
data () {
return {
message: 'A'
}
}
}
</script>
<style scoped>
</style>
The test service lives in the same directory and is very simple:
class Service {
getMessage () {
return new Promise((resolve, reject) => {
console.log('hello from test service')
resolve('B')
})
}
}
const service = new Service()
export default service
So in order to mock this service, im using the webpack vue-loader to inject the mock service as described in the official documentation here:
https://vue-loader.vuejs.org/en/workflow/testing-with-mocks.html
So here is my test which is almost identical to the example:
import Vue from 'vue'
const HelloInjector = require('!!vue-loader?inject!../../../src/components/HelloWorld')
const Hello = HelloInjector({
// mock it
'./test.service': {
getMessage () {
return new Promise((resolve, reject) => {
resolve('C')
})
}
}
})
describe('HelloWorld.vue', () => {
it('should render', () => {
const vm = new Vue({
template: '<div><test></test></div>',
components: {
'test': Hello
}
}).$mount()
expect(vm.$el.querySelector('h1').textContent).to.equal('C')
})
})
There are two issues i am facing:
The test fails because the assertion is executing before the mocked promise is resolved. From my understanding, this is because the vue lifecycle hasnt completely finished when im doing my asserting. The common patter to wait for the next cycle would be to wrapp my assertion around the next tick function like this:
it('should render', (done) => {
const vm = new Vue({
template: '<div><test></test></div>',
components: {
'test': Hello
}
}).$mount()
Vue.nextTick(() => {
expect(vm.$el.querySelector('h1').textContent).to.equal('C')
done()
})
})
This, however, does not work unless I nest 3 nextTicks, which seems extremely hacky to me. Is there something I am missing to get this to work? this example seems extremely straightforward, but I cannot get this test to pass without lots of nextTicks
I keep getting a strange error, intermittently... this warning shows up probably 50% of the time and is not consistant at all.
[vue warn] Failed to mount component: template or render function is not defined
Again, this happens only sometimes. I can run the same exact unit test without any changes and it will show me this message 50% of the time.
I truly couldn't figure out why sometimes the component failed to mount. I'm not even sure it's related to the injector but, in any case, I kept the test consistent by not using it; trying a different approach instead.
The component might be more testable if the service is injected through the props instead of being directly used.
<template>
<div>
<h1>{{ message }}</h1>
</div>
</template>
<script>
import service from './test.service'
export default {
name: 'HelloWorld',
created () {
this.service.getMessage().then(message => {
this.message = message
})
},
data () {
return {
message: 'A'
}
},
props: {
service: {
default: service
}
}
}
</script>
<style scoped>
</style>
This makes the injector unnecessary as the mocked service can be instead passed to the component using propsData in the constructor.
import Vue from 'vue'
import Async from '#/components/Async'
const service = {
getMessage () {
return new Promise((resolve, reject) => {
resolve('C')
})
}
}
describe('Async.vue', () => {
let vm
before(() => {
const Constructor = Vue.extend(Async)
vm = new Constructor({
propsData: {
service: service
}
}).$mount()
})
it('should render', function () {
// Wrapping the tick inside a promise, bypassing PhantomJS's lack of support
return (new Promise(resolve => Vue.nextTick(() => resolve()))).then(() => {
expect(vm.$el.querySelector('h1').textContent).to.equal('C')
})
})
})

Testing redux connected component

I have the following connected component in React-Redux
export class IncrementalSearch extends React.Component {
constructor(props) {
super(props);
this.onSearch$ = new Subject();
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
this.subscription = this.onSearch$
.debounceTime(300)
.subscribe(debounced => {
this.props.onPerformIncrementalSearch(debounced);
});
}
componentWillUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
onChange(e) {
const newText = e.target.value;
this.onSearch$.next(newText);
}
render() {
return (
<div className={styles.srchBoxContaner}>
<input
className={styles.incSrchTextBox}
type="text" name="search" id="searchInput" placeholder="Search.."
onChange={this.onChange}
/>
</div>
);
}
}
const mapDispatchToProps = (dispatch) => ({
onPerformIncrementalSearch: (searchText) => {
dispatch(performIncrementalStoreSearch(searchText));
}
});
const IncrementalSearchComponent = connect(null, mapDispatchToProps)(IncrementalSearch);
export default IncrementalSearchComponent;
I'm now trying to write a unit tests for the connected component. I'm using Jest, Enzyme, and Sinon. So far this is what my unit test looks like
it('calls \'onPerformIncrementalSearch\' when the user types in something', () => {
const mockStore = configureStore();
const onPerformIncrementalSearchSpy = sinon.spy();
const mapStateToProps = null;
const mapDispatchToProps = {
onPerformIncrementalSearch: onPerformIncrementalSearchSpy
};
const mappedProps = { mapStateToProps, mapDispatchToProps };
const incrementalSearchWrapper =
mount(
<Provider store={mockStore}>
<IncrementalSearchComponent
onPerformIncrementalSearch={onPerformIncrementalSearchSpy}
props={mappedProps}
store={mockStore}
/>
</Provider>
);
//find the input element
const searchInput = incrementalSearchWrapper.find('#searchInput');
searchInput.node.value = 'David';
searchInput.simulate('change', searchInput);
expect(onPerformIncrementalSearchSpy.called).toEqual(true);
// onChangeSpy.restore();
});
However, when I run this test, I get the following error
TypeError: Cannot read property 'bind' of undefined
How do I fix this?
Testing connected components can be a huge pain. I find that it's more trouble than it's worth to try to wrap your components with a Provider to give them access to the store.
Instead, I would just export the component, mapStateToProps, and mapDispatchToProps and test them individually. Your app will still work the same if you export the connected component as the default.
Dan Abramov (Co author of Redux) suggests this approach in this comment
I would also suggest looking into enzyme shallow rendering instead of using mount when testing connected components.

How to test react component correctly?

Recently I am learning to test React with jest and enzyme, It seems hard to understand what a unit test is it, my code
import React from "react";
class App extends React.Component {
constructor() {
super();
this.state = {
value: ""
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
const value = e.target.value;
this.setState({
value
});
}
render() {
return <Nest value={this.state.value} handleChange={this.handleChange} />;
}
}
export const Nest = props => {
return <input value={props.value} onChange={props.handleChange} />;
};
export default App;
and my test
import React from "react";
import App, { Nest } from "./nest";
import { shallow, mount } from "enzyme";
it("should be goood", () => {
const handleChange = jest.fn();
const wrapper = mount(<App />);
wrapper.find("input").simulate("change", { target: { value: "test" } });
expect(handleChange).toHaveBeenCalledTimes(1);
});
IMO, the mocked handleClick will intercept the handleClick on App,
if this is totally wrong, what's the right way to use mock fn and test the handleClick be called.
Another: I search a lot, read the similar situations, seem like this iscontra-Unit Test,
Probably I should test the two component separately, I can test both components,
test the
<Nest value={value} handleChange={handleChange} />
by pass the props manually, and then handleChangeinvoked by simulate change
it passed test.
but how can I test the connection between the two?
I read
some work is React Team's Work
...
I don't know which parts I have to test in this case, and Which parts react already tested and don't need me to test. That's confusing.
You should take the path of testing the Nest component in isolation first, passing your mocked handleChange as a prop, to verify that input changes are being propagated.
If you want to test the state part, then you can get the instance of your App class from enzyme and call that method directly:
it("should update the Nest value prop when change is received", () => {
const wrapper = mount(<App />);
const instance = wrapper.instance()
instance.handleChange( { target: { value: "test" } })
const nestComponent = wrapper.find("Nest").first()
expect(nestComponent).prop('value').toEqual('test');
});
This a very very basic, almost not needed to test piece of code, but it will get your test coverage up if that's what you're after.
Doc for instance: http://airbnb.io/enzyme/docs/api/ReactWrapper/instance.html
If you want to test for the connection. From what I see, the nest component is a child component inside the App component. You could test that <App /> contains `.
describe('<App />', () => {
it('should contain a nest component', () => {
const wrapper = mount(<App />);
expect(wrapper.find(<Nest />)).toHaveLength(1);
});
});
Secondly, since the onChange event on the nest component updates the state in the App component, you can also test for state changes since its a behavior you expect.
it('should update state', () => {
//find input and simulate change with say {value: 'new value'} and then
expect(wrapper.state().value).toBe('newValue');
});
I hope this helps.

React Router v4 Redirect unit test

How do I unit test the component in react router v4?
I am unsuccessfully trying to unit test a simple component with a redirect using jest and enzyme.
My component:
const AppContainer = ({ location }) =>
(isUserAuthenticated()
? <AppWithData />
: <Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>);
My attempt to test it:
function setup() {
const enzymeWrapper = mount(
<MemoryRouter initialEntries={["/"]}>
<AppContainer />
</MemoryRouter>
);
return {
enzymeWrapper
};
}
jest.mock("lib/authAPI", () => ({
isUserAuthenticated: jest.fn(() => false)
}));
describe("AppContainer component", () => {
it("renders redirect", () => {
const { enzymeWrapper } = setup();
expect(enzymeWrapper.find("<Redirect></Redirect>")).toBe(true);
});
});
Answering my own question.
Basically I'm making a shallow render of my component and verifying that if authenticated is rendering the redirect component otherwise the App one.
Here the code:
function setup() {
const enzymeWrapper = shallow(<AuthenticatedApp />);
return {
enzymeWrapper
};
}
describe("AuthenticatedApp component", () => {
it("renders Redirect when user NOT autheticated", () => {
authApi.isUserAuthenticated = jest.fn(() => false);
const { enzymeWrapper } = setup();
expect(enzymeWrapper.find(Redirect)).toHaveLength(1);
});
it("renders AppWithData when user autheticated", () => {
authApi.isUserAuthenticated = jest.fn(() => true);
const { enzymeWrapper } = setup();
expect(enzymeWrapper.find(AppWithData)).toHaveLength(1);
});
});
Neither of these answers worked for me and took a fair bit of digging so I thought I'd chip in my experience here.
PrivateRoute.js
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
auth.isAuthenticated
? <Component {...props} />
: <Redirect to={{
pathname: '/',
state: { from: props.location }
}} />
)} />
)
PrivateRoute.spec.js
This test worked for me with no problems whatsoever, it rendered the PrivateComponent when auth.isAuthenticated evaluated to true.
it('renders the component when the user is authorised', () => {
auth.login()
expect(auth.isAuthenticated).toBe(true)
const privateRoute = mount(
<MemoryRouter initialEntries={['/privateComponent']}>
<PrivateRoute path='/privateComponent' component={PrivateComponent} />
</MemoryRouter>
)
expect(privateRoute.find('PrivateComponent').length).toEqual(1)
})
This was the test that gave me a lot of issues. At first I was checking for the Redirect component.
I tried to just do something like
expect(privateRoute.find('Redirect').length).toEqual(1)
But that just wouldn't work, no matter what I did, it just couldn't find the Redirect component. In the end, I ended up checking the history but couldn't find any reliable documentation online and ended up looking at the React Router codebase.
In MemoryRouter.js (line 30) I saw that it rendered a Router component. I noticed that it was also passing it's history as a prop to Router so I figured I would be able to grab it from there.
I ended up grabbing the history prop from Router using privateRoute.find('Router').prop('history') which then finally gave me evidence that a redirect had actually happened, to the correct location, no less.
it('renders a redirect when the user is not authorised', () => {
auth.logout()
expect(auth.isAuthenticated).toBe(false)
const privateRoute = mount(
<MemoryRouter initialEntries={['/privateComponent']}>
<PrivateRoute path='/privateComponent' component={PrivateComponent} />
</MemoryRouter>
)
expect(privateRoute.find('PrivateComponent').length).toEqual(0)
expect(
privateRoute.find('Router').prop('history').location.pathname
).toEqual('/')
})
With this test, you're testing the actual functionality of the PrivateRoute component and ensuring that it goes where it's saying it's going.
The documentation leaves a lot to be desired. For example, it took a fair bit of digging for me to find out about initialEntries as a prop for MemoryRouter, you need this so it actually hits the route and executes the conditional, I spent too long trying to cover both branches only to realise this was what was needed.
Hope this helps someone.
Here's my minimal example of testing that the actual URL changes instead of just that a Redirect component exists on the page:
RedirectApp.js:
import React from "react";
import { Route, Switch, Redirect } from "react-router-dom";
const RedirectApp = props => {
return (
<Switch>
<Redirect from="/all-courses" to="/courses" />
</Switch>
);
};
export default RedirectApp;
RedirectApp.test.js:
import React from "react";
import { MemoryRouter, Route } from "react-router-dom";
import { mount } from "enzyme";
import RedirectApp from "./RedirectApp";
it("redirects /all-courses to /courses", () => {
const wrapper = mount(
<MemoryRouter initialEntries={[`/all-courses`]}>
<Route component={RedirectApp} />
</MemoryRouter>
);
expect(wrapper.find(RedirectApp).props().location.pathname).toBe("/courses");
});
By wrapping RedirectApp in a Route, MemoryRouter injects the react-router props (match, location, and history) in RedirectApp.
enzyme lets you grab these props(), and the location prop includes the pathname after redirect, so the redirected location can be matched.
This method is a little hacky, but has the advantage of testing that a redirect is going to the correct place and not just that a Redirect exists.
Alternatively, you can export default withRouter(RedirectApp) in RedirectApp.js to automatically get the react-router props injected.