redux saga put not dispatching action - enzyme

I'm trying to to do integration tests, by mounting a smart connected component.
The fetch action that is within componentDidMount of my smart components dispatches just fine, and it's taken by my Saga. Although it is supposed to put a success action, it doesn't .
Here is my testing code :
import React from 'react'
import { Provider } from 'react-redux'
import configureMockStore from 'redux-mock-store'
import MockAdapter from 'axios-mock-adapter'
import Enzyme,{ mount,render } from 'enzyme'
import Tasks from '../containers/tasks.jsx'
import createSagaMiddleware from 'redux-saga'
import axios from "axios";
import Adapter from 'enzyme-adapter-react-16';
import mySaga from '../actions/tasksSaga'
import { createStore, combineReducers, applyMiddleware } from 'redux'
import reducer from '../reducers'
Enzyme.configure({ adapter: new Adapter() });
describe('App', () => {
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
const mock = new MockAdapter(axios)
const state = {
tasksReducer:{
tasks:[]
},
uiReducer :{
}
};
const todos = [
{
id: 1,
title: 'todo1',
description: 'nice'
},
{
id: 2,
title: 'todo2',
description: 'nice'
}
]
beforeAll(() => {
mock.onGet('http://localhost:9001/tasks').reply(200, {tasks:todos})
})
it('renders an App container', () => {
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware),
)
sagaMiddleware.run(mySaga)
const wrapper = mount(
<Provider store={store}>
<Tasks />
</Provider>
)
wrapper.instance().forceUpdate()
expect(wrapper.find('Task')).toHaveLength(3)
})
})
My success action is never called although data is good.

Related

Vue Composition Api with Vuetify fails unit testing with jest

I try to run unit tests on vue components, the compnents are written with #vue/composition-api package and they also use vuetify.
The Application runs like expected, but when I run the tests I don't have access to the breakpoint property under context.root.$vuetify. When I print the context.root.$vuetify a see the vue component instance.
The error is "Cannot read property 'mdAndDown' of undefined" when i try to access it like that:
context.root.$vuetify.breakpoint.mdAndDown
This is is my jest config file:
module.exports = {
preset: '#vue/cli-plugin-unit-jest/presets/typescript-and-babel',
transform: { '^.*\\.js$': 'babel-jest' },
transformIgnorePatterns: ['node_modules/(?!vue-router|#babel|vuetify)'],
setupFiles: [
"<rootDir>/tests/unit/setup-env.ts",
],
};
This is the Component file:
<template>
<v-app
class="sst-app-container"
>
<cmp-loading
v-show="!loadedBasic"
/>
<div
id="app-wrp"
v-show="loadedBasic"
>
<cmp-side-bar />
<v-content
class="fill-height"
>
<router-view></router-view>
</v-content>
</div>
</v-app>
</template>
<script lang="ts">
import {
computed, createComponent, onMounted, reactive, toRefs, watch,
} from '#vue/composition-api';
import basicSetup from '#/modules/core/composables/basic-setup';
import initMd from '#/modules/core/devices/mouse/composables/init-md';
import store from '#/stores';
import cmpSideBar from '../components/sidebar/CoreSideBar.mouse.vue';
import cmpLoading from '../components/loading/CoreLoading.vue';
export default createComponent({
components: {
cmpLoading,
cmpSideBar,
},
setup(props, context) {
console.log(context.root.$vuetify)
const basics = basicSetup();
return {
...basics,
};
},
});
</script>
This is my test:
import Vue from 'vue';
import Vuetify from 'vuetify';
import BtnCmp from '../../../../../components/vc-btn.vue';
import CmApi from '#vue/composition-api';
import { library } from '#fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '#fortawesome/vue-fontawesome';
import {
faPlug,
faSignOut,
faCog,
faUser,
faTachometer,
faArrowLeft,
faArrowRight,
} from '#fortawesome/pro-duotone-svg-icons';
library.add(
faPlug,
faSignOut,
faCog,
faUser,
faTachometer,
faArrowLeft,
faArrowRight,
);
import { Breakpoint, Theme, Application, Goto, Icons, Lang, Presets } from 'vuetify/lib/services';
import {
mount,
createLocalVue,
} from '#vue/test-utils';
import Core from '../views/Core.mouse.vue';
const vue = createLocalVue();
vue.component('font-awesome-icon', FontAwesomeIcon);
vue.component('vc-btn', BtnCmp);
vue.use(Vuetify);
vue.use(CmApi);
describe('Core', () => {
let vuetify;
beforeEach(() => {
vuetify = new Vuetify({
mocks: {
breakpoint: new Breakpoint({
breakpoint: {
scrollBarWidth: 0,
thresholds: {
xs: 0,
sm: 0,
md: 0,
lg: 0,
},
},
},
});
});
it('renders the correct markup', async () => {
// Now mount the component and you have the wrapper
const wrapper = mount(Core, {
localVue: vue,
vuetify,
stubs: ['router-link', 'router-view'],
mocks: {
$t: () => 'some specific text'
},
});
expect(wrapper.html()).toContain('....');
});
});
This can be solved by adding a new instance of vuetify to the wrapper vuetify: new Vuetify()

Jest testing connected redux container component

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as CommonActions from 'common/actions/common.actions';
import Activity from './activity.component';
const mapStateToProps = ({ common }) => ({common});
const mapDispatchToProps = dispatch => bindActionCreators(
{
getNationalities: CommonActions.getNationalities,
},dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Activity);
How Can I test a container component like this which contains mapStateToProps, mapDispatchToProps and connect ??
React-test-render can test component, You have to import component that would include your mapStateToProps, mapDispatchToProps.
Example:
<YourComponentName
common={jest.fn()}
getNationalities={jest.fn()}
/>

How to test mutations when using vuex modules

Before using vuex modules , my mutation tests were OK :
import mutations from '#/vuex/mutations.js'
import vueAuthInstance from '#/services/auth.js'
import { IS_AUTHENTICATED, CURRENT_USER_ID } from '#/vuex/mutation_types.js'
describe('mutations.js', () => {
var state
beforeEach(() => {
state = {
isAuthenticated: vueAuthInstance.isAuthenticated(),
currentUserId: ''
}
})
describe('IS_AUTHENTICATED', () => {
it('should set authentication status', () => {
state.isAuthenticated = false
mutations[IS_AUTHENTICATED](state, {isAuthenticated: true})
expect(state.isAuthenticated).to.eql(true)
})
})
...
})
Now I refactored my vuex folders, my state and mutations are inside each vuex/modules/../index.js file
src
|_ vuex
| L_ modules
| L_ login
| |_ index.js
| |_ actions.js
| |_ getters.js
| |_ mutation_types.js
|_ App.vue
|_ main.js
vuex/modules/login/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import actions from './actions'
import getters from './getters'
import * as types from './mutation_types'
import vueAuthInstance from '#/services/auth.js'
Vue.use(Vuex)
const state = {
isAuthenticated: vueAuthInstance.isAuthenticated(),
currentUserId: ''
}
const mutations = {
[types.IS_AUTHENTICATED] (state, payload) {
state.isAuthenticated = payload.isAuthenticated
},
...
}
export default {
state,
mutations,
actions,
getters
}
Withe a vuex/store.js
import Vue from 'vue'
import Vuex from 'vuex'
import login from '#/vuex/modules/login'
// import other modules
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
login,
... (other modules )
}
})
To take in account this new structure, I rewrote the unit test as following :
test/unit/specs/vuex/modules/login/index.spec.js
import { mutations } from '#/vuex/modules/login/index.js'
import vueAuthInstance from '#/services/auth.js'
import types from '#/vuex/modules/login/mutation_types.js'
describe('mutations.js', () => {
var state
beforeEach(() => {
state = {
isAuthenticated: vueAuthInstance.isAuthenticated(),
currentUserId: ''
}
})
describe('IS_AUTHENTICATED', () => {
it('should set authentication status', () => {
state.isAuthenticated = false
mutations[types.IS_AUTHENTICATED](state, {isAuthenticated: true})
expect(state.isAuthenticated).to.eql(true)
})
})
})
And I get an error, on the mutation :
✗ should set authentication status
TypeError: Cannot read property 'IS_AUTHENTICATED' of undefined
I tried to change the import { mutations } statement , and import directly the store.js, in which the modules are defined, and use store._mutations,
LOG: 'MUTATIONS: ', Object{IS_AUTHENTICATED: [function wrappedMutationHandler(payload) { ... }], ...}
using store._mutations.IS_AUTHENTICATED0 , seems to work, ( don't know why an array? ..) but something is wrong with this function and the state, payload args, as the test doesn't pass
import store from '#/vuex/store'
import vueAuthInstance from '#/services/auth.js'
describe('mutations.js', () => {
var state
beforeEach(() => {
state = {
isAuthenticated: vueAuthInstance.isAuthenticated(),
currentUserId: ''
}
})
describe('IS_AUTHENTICATED', () => {
it('should set authentication status', () => {
state.isAuthenticated = false
console.log('MUTATIONS: ', store._mutations.IS_AUTHENTICATED())
store._mutations.IS_AUTHENTICATED[0](state, {isAuthenticated: true})
expect(state.isAuthenticated).to.eql(true)
})
})
...
})
1) should set authentication status
mutations.js IS_AUTHENTICATED
AssertionError: expected false to deeply equal true
I checked the passed args to the mutation in the index.js file
const mutations = {
[types.IS_AUTHENTICATED] (state, payload) {
console.log('MUTATION state: ', state)
console.log('MUTATION payload: ', payload)
state.isAuthenticated = payload.isAuthenticated
},
[types.CURRENT_USER_ID] (state, payload) {
state.currentUserId = payload.currentUserId
}
}
And. I do not see the passed arg values, it seems that the state args is the only passed value from my test :
LOG: 'MUTATION state: ', Object{isAuthenticated: false, currentUserId: ''}
LOG: 'MUTATION payload: ', Object{isAuthenticated: false, currentUserId: ''}
What's wrong with this code ? how to proceed for testing the mutations in this case, using vuex modules ?
thanks for feedback
I found a way to test the mutation using vuex modules, but I don't know if it's the best way...
my test is quite simple , using store.commit as I cannot call directly the mutation handler, and I import only the vuex/store
src/test/unit/specs/modules/login/index.js
import store from '#/vuex/store'
describe('mutations.js', () => {
describe('IS_AUTHENTICATED', () => {
it('should set authentication status', () => {
store.commit('IS_AUTHENTICATED', {isAuthenticated: true})
expect(store.state.login.isAuthenticated).to.eql(true)
})
})
...
})
Actually, this is bad approach.
You should create mock state, and use it.
import { createLocalVue } from '#vue/test-utils';
import Vuex from 'vuex';
import { storeModule } from '#path/to/store/modules';
const mutations = storeModule.mutations;
describe('mutations', () => {
it('testCase#1', () => {
createLocalVue().use(Vuex);
const state = {
//mock state values
};
const store = new Vuex.Store({state, mutations});
store.commit('mutationToTest', arg);
expect(state.arg).toBe(1);
})
})

Mock redux saga middleware with Jest

I've got this configureStore.js which configures my enhanced redux store and persists it to the localStorage:
// #flow
import 'babel-polyfill'
import { addFormSubmitSagaTo } from 'redux-form-submit-saga/es/immutable'
import { applyMiddleware, createStore, compose } from 'redux'
import { autoRehydrate, persistStore } from 'redux-persist-immutable'
import { browserHistory } from 'react-router'
import { combineReducers } from 'redux-immutable'
import { fromJS } from 'immutable'
import { routerMiddleware } from 'react-router-redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer from './reducers'
import sagas from './sagas'
export default function configureStore () {
const initialState = fromJS({ initial: { dummy: true } })
const sagaMiddleware = createSagaMiddleware()
const middleware = [ routerMiddleware(browserHistory), sagaMiddleware ]
const enhancer = compose(
autoRehydrate(),
applyMiddleware(...middleware),
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__()
)
const store = createStore(
combineReducers(rootReducer),
initialState,
enhancer
)
// Persist store to the local storage
persistStore(store, { blacklist: [ 'form', 'routing' ] })
// Decorate with Redux Form Submit Saga
// and create hook for saga's
const rootSaga = addFormSubmitSagaTo(sagas)
sagaMiddleware.run(rootSaga)
return store
}
Now I'm trying to test this file using Jest:
import configureStore from './../configureStore'
import * as reduxPersistImmutable from 'redux-persist-immutable'
import * as redux from 'redux'
import createSagaMiddleware from 'redux-saga'
describe('configureStore', () => {
it('persists the store to the localStorage', () => {
reduxPersistImmutable.persistStore = jest.fn()
redux.createStore = jest.fn()
createSagaMiddleware.default = jest.fn(() => {
run: () => jest.fn()
})
configureStore()
})
})
Everything runs smooth in this test until configureStore reaches the sagaMiddleware.run(rootSaga) line. It throws the following error:
FAIL app/__tests__/configureStore.js
● Console
console.error ../node_modules/redux-saga/lib/internal/utils.js:206
uncaught at check Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware
● configureStore › persists the store to the localStorage
Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware
at check (../node_modules/redux-saga/lib/internal/utils.js:50:11)
at Function.sagaMiddleware.run (../node_modules/redux-saga/lib/internal/middleware.js:87:22)
at configureStore (app/configureStore.js:38:18)
at Object.<anonymous> (app/__tests__/configureStore.js:17:60)
at process._tickCallback (../internal/process/next_tick.js:103:7)
This error indicates that the mock function does not work as I intend: it doesn't seem to be overwritten and is called from within redux-saga. Mocking redux.createStore works just fine.
Apparently, using jest.mock('redux-saga', () => () => ({ run: jest.fn() })) is the right way to mock redux-saga's createSagaMiddleware function.

Redux: How to test a connected component?

I am using Enzyme to unit test my React components. I understand that in order to test the raw unconnected component I'd have to just export it and test it (I've done that). I have managed to write a test for the connected component but I am really not sure if this's the right way and also what exactly would I want to test for the connected component.
Container.jsx
import {connect} from 'react-redux';
import Login from './Login.jsx';
import * as loginActions from './login.actions';
const mapStateToProps = state => ({
auth: state.auth
});
const mapDispatchToProps = dispatch => ({
loginUser: credentials => dispatch(loginActions.loginUser(credentials))
});
export default connect(mapStateToProps, mapDispatchToProps)(Login);
Container.test.js
import React from 'react';
import {Provider} from 'react-redux';
import {mount, shallow} from 'enzyme';
import {expect} from 'chai';
import LoginContainer from '../../src/login/login.container';
import Login from '../../src/login/Login';
describe('Container Login', () => {
it('should render the container component', () => {
const storeFake = state => ({
default: () => {
},
subscribe: () => {
},
dispatch: () => {
},
getState: () => ({ ...state })
});
const store = storeFake({
auth: {
sport: 'BASKETBALL'
}
});
const wrapper = mount(
<Provider store={store}>
<LoginContainer />
</Provider>
);
expect(wrapper.find(LoginContainer).length).to.equal(1);
const container = wrapper.find(LoginContainer);
expect(container.find(Login).length).to.equal(1);
expect(container.find(Login).props().auth).to.eql({ sport: 'BASKETBALL' });
});
});
This is an interesting question.
I usually do import both container and component to do the testing. For container testing I use, redux-mock-store. Component testing is for testing async functions. For instance in your case, login process is an async function using sinon stubs. Here is a snippet of the same,
import React from 'react';
import {Provider} from 'react-redux';
import {mount, shallow} from 'enzyme';
import {expect} from 'chai';
import LoginContainer from '../../src/login/login.container';
import Login from '../../src/login/Login';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { stub } from 'sinon';
const mockStore = configureMockStore([thunk]);
describe('Container Login', () => {
let store;
beforeEach(() => {
store = mockStore({
auth: {
sport: 'BASKETBALL',
},
});
});
it('should render the container component', () => {
const wrapper = mount(
<Provider store={store}>
<LoginContainer />
</Provider>
);
expect(wrapper.find(LoginContainer).length).to.equal(1);
const container = wrapper.find(LoginContainer);
expect(container.find(Login).length).to.equal(1);
expect(container.find(Login).props().auth).to.eql({ sport: 'BASKETBALL' });
});
it('should perform login', () => {
const loginStub = stub().withArgs({
username: 'abcd',
password: '1234',
});
const wrapper = mount(<Login
loginUser={loginStub}
/>);
wrapper.find('button').simulate('click');
expect(loginStub.callCount).to.equal(1);
});
});
As you pointed out, the way I usually do this is to export the un-connected component as well, and test that.
i.e.
export {Login};
Here's an example. Source of the component, and source of the tests.
For the wrapped component, I don't author tests for those because my mappings (mapStateToProps and mapDispatchToProps) are generally very simple. If I wanted to test a wrapped component, I'd really just be testing those maps. So those are what I would choose to explicitly test, rather than re-testing the entire component in a wrapped form.
There are two ways to test those functions. One way would be to export the functions within the module itself.
i.e.;
export {mapStateToProps, mapDispatchToProps}
I'm not a huge fan of this, because I wouldn't want other modules in the app to access them. In my tests, I sometimes use babel-plugin-rewire to access "in-scope" variables, so that's what I would do in this situation.
That might look something like:
import {
Login, __Rewire__
}
const mapStateToProps = __Rewire__.__get__('mapStateToProps');
describe('mapStateToProps', () => { ... });
If we have a router issue, we can consider to add the router lib into the test file, eg:
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { mount } from 'enzyme';
import ReadDots from './ReadDots';
const storeFake = state => ({
default: () => {
},
subscribe: () => {
},
dispatch: () => {
},
getState: () => ({ ...state })
});
const store = storeFake({
dot: {
dots: [
{
id: '1',
dot: 'test data',
cost: '100',
tag: 'pocket money'
}
]
}
});
describe('<ReadDots />', () => {
it('should render ReadDots component', () => {
const component = mount(
<Provider store={store}>
<Router>
<ReadDots />
</Router>
</Provider>
);
expect(component.length).toEqual(1);
});
});