React Router v4 Redirect unit test - unit-testing

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.

Related

How to mock an async action creator with jest

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.

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')
})
})
})

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.

Unit Test react-router onEnter/onLeave

I've some logic runs at onEnter/onLeave. I've used some setInterval at onEnter and clear them at onLeave using clearInterval.
How can I unit test above case?
<Route
component={App}
onEnter={onEnterApp}
onLeave={onLeaveApp}
path="/app">
Below is my test case but it fails ,
import React from 'react';
import App from '../components/views/app.jsx';
import {shallow, mount, render} from 'enzyme';
import {expect, assert} from 'chai';
import sinon from 'sinon';
import {mountWithIntl} from '../test_utils/intl-enzyme-test-helper.js';
describe(‘App renders correctly', () => {
it('When mounting set intervals', () => {
const wrapper = mountWithIntl(<App/>);
expect(window.x).to.exist;
expect(window.y).to.exist;
});
it('When unmounting clear intervals', () => {
const wrapper = mountWithIntl(<App/>);
wrapper.unmount();
expect(window.x).to.not.exist;
expect(window.y).to.not.exist;
});
});
The onEnter and onLeave props aren't tied to the componentWillMount and componentWillUnmount methods of your <App> component, so just mounting and unmounting the <App> will not call those functions.
Assuming you trust that React Router works, you can just test that your onEnterApp and onLeaveApp functions work properly
describe('onEnterApp', () => {
it('sets x and y', () => {
onEnterApp();
expect(global.x).to.exist;
expect(globa.y).to.exist;
});
});
If you want to verify that they are run when the URL matches the /app path, then you will need to involve a <Router>.
import createMemoryHistory from 'react-router';
describe('App', () => {
it('runs onEnterApp when navigating to /app', (done) => {
const history = createMemoryHistory(['/app']);
const holder = document.createElement('div');
render((
<Router history={history}>
<Route
component={App}
onEnter={onEnterApp}
onLeave={onLeaveApp}
path="/app">
</Router>
), holder, () => {
expect(global.x).to.exist;
expect(globa.y).to.exist;
done();
});
});
});
Testing onLeaveApp would require you to navigate to a new location with your history instance and then test that the desired global state exists.
history.push({ pathname: '/foo' })

Mocha - how to check if element exist after ajax call

How to check if select have some option in it after it has been updated by ajax function. My select component is like below
class Select extends React.component(){
constructor(){
super()
this.state.item=[]
}
componentWillMount(){
axios.get(list)
.then(function (response) {
this.setState({item:response.data})
// reponse data ['america','singapore','vietnam']
})
}
render(){
return(
<select>
this.state.item.map((i) => {
<option value="i">i</option>
})
</select>
)
}
}
Here is my try:
import {expect} from 'chai'
import {shallow} from 'enzyme'
import sinon from 'sinon'
import Select from 'Select'
describe('select list', () => {
it('should have options', () => {
const wrapper = shallow(<Select />)
wrapper.update()
const actual = wrapper.find('option').length
expect(actual).to.not.equal(0)
})
})
What wrong with this is, I got var actual = 0. It supposes to be 3. So I guess I missing something with axios. What should I add to my spec?
Your GET request might be still waiting for the response but mocha already has completed the test execution.
You could add a timeout and assert after a while and then call the done() callback when you are done with the test. Please take a look at the mocha's asynchronous code section.
describe('select list', () => {
it('should have options', (done) => {
const wrapper = shallow(<Select />)
wrapper.update()
setTimeout(() => {
const actual = wrapper.find('option').length
expect(actual).to.not.equal(0)
done();
}, 2000)
})
})
I recommend you to check the axios-mock-adapter which allows you to mock request rather than sending an actual request to the server.