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.
Related
I am currently working on a project that is using Vue, Class based components, typescript, pug, vuetify and Jest for unit testing. I have been trying to run unit tests using jest and have not been able to get them to work. At this point I am pretty lost as to what could be wrong. It seems that there are issues with unit tests when using vueifty which I think I have sorted out but am not certain. When I run the test the test fails because the wrapper is always empty.
Component
<template lang="pug">
v-row(align="center" justify="center")
v-col(cols="6")
v-card
v-form(ref="loginForm" v-model="valid" v-on:keyup.enter.native="login")
v-card-title#title Login
v-card-text
v-text-field(class="mt-4" label="Username" required outlined v-model="username" :rules="[() => !!username || 'Username Required.']")
v-text-field(label="Password" required outlined password :type="show ? 'text' : 'password'" :append-icon="show ? 'visibility' : 'visibility_off'" #click:append="show = !show" v-model="password" :rules="[() => !!password || 'Password Required.']")
v-alert(v-if="error" v-model="error" type="error" dense dismissible class="mx-4")
| Error while logging in: {{ errorMsg }}
v-card-actions()
div(class="flex-grow-1")
v-btn(class="mr-4" color="teal" :disabled="!valid" large depressed #click="login") Login
div Forgot password?
a(href="/forgot-password" class="mx-2") Click here
div(class="my-2") Don't have an account?
a(href="/signup" class="mx-2") Signup
| for one
</template>
<script lang="ts">
import { AxiosError, AxiosResponse } from 'axios';
import JwtDecode from 'jwt-decode';
import { Component, Vue } from 'vue-property-decorator';
import { TokenDto, VForm } from '#/interfaces/GlobalTypes';
#Component({
name: 'LoginForm',
})
export default class Login extends Vue {
private password: string = '';
private username: string = '';
private show: boolean = false;
private error: boolean = false;
private errorMsg: string = '';
private valid: boolean = false;
... removed rest for brevity
Test
import LoginForm from '#/components/auth/LoginForm.vue';
import login from '#/views/auth/LoginView.vue';
import { createLocalVue, mount } from '#vue/test-utils';
import Vue from 'vue';
import Vuetify from 'vuetify';
// jest.mock('axios')
Vue.use(Vuetify)
const localVue = createLocalVue();
console.log(localVue)
describe('LoginForm.vue', () => {
let vuetify: any
beforeEach(() => {
vuetify = new Vuetify()
});
it('should log in successfully', () => {
const wrapper = mount(LoginForm, {
localVue,
vuetify
})
console.log(wrapper.find('.v-btn'))
});
});
The LoginForm is loaded properly but it does not seeem that that mount creates the wrapper for some reason. When I log the wrapper I get:
VueWrapper {
isFunctionalComponent: undefined,
_emitted: [Object: null prototype] {},
_emittedByOrder: []
}
Any ideas are greatly appericated
you can try:
wrapper.findComponent({name: 'v-btn'})
I guess I am late but I made it work.
I noticed you tried to find VBtn component by 'v-btn' class but VBtn doesn't have it by default. That's why I decided to stub it with my own VBtn that has 'v-btn' class.
import { shallowMount, Wrapper } from '#vue/test-utils'
import Login from '#/components/Login/Login.vue'
import Vue from 'vue'
import Vuetify from 'vuetify'
Vue.use(Vuetify)
let wrapper: Wrapper<Login & { [ key: string]: any }>
const VButtonStub = {
template: '<button class="v-btn"/>'
}
describe('LoginForm.vue', () => {
beforeEach(() => {
wrapper = shallowMount(Login, {
stubs: {
VBtn: VButtonStub
}
})
})
it('should log in successfully', () => {
console.log(wrapper.html())
})
})
After test passed you will see in console log that stubbed component has 'v-btn' class. You can add yours and work with it like you want.
I am writing unit tests for VueJS components and have consulted the "Applying Global Plugins and Mixins" section of Vue Test Utils Common Tips. I have a component that depends on the Vuex store so it makes sense that I would transpose the example under that section for my purposes.
Here is my code for that component's specific .spec.js file:
import { createLocalVue, mount } from '#vue/test-utils'
import AppFooter from '#/components/AppFooter/AppFooter'
import store from '#/store'
describe('AppFooter component', () => {
const localVue = createLocalVue()
localVue.use(store)
it('AppFooter should have header slot', () => {
const AppFooterComponent = mount(AppFooter, {
localVue
})
/* TODO: Replace with a more appropriate assertion */
expect(true).toEqual(true)
})
})
This is pretty faithful to the example provided in the link above. However, the error I receive when I run the test suite is as follows:
Should I be installing the Vue store differently?
To elaborate on my comment, I believe it should look like the following, where you pass in the store on the mount() call.
import { createLocalVue, mount } from '#vue/test-utils'
import AppFooter from '#/components/AppFooter/AppFooter'
import Vuex from 'vuex'
import store from '#/store' //you could also mock this out.
describe('AppFooter component', () => {
const localVue = createLocalVue()
localVue.use(Vuex)
it('AppFooter should have header slot', () => {
const AppFooterComponent = mount(AppFooter, {
store,
localVue
})
/* TODO: Replace with a more appropriate assertion */
expect(true).toEqual(true)
})
})
I believe that you have something like this.$store.getters[someBeautifulGetterName] in you component. To make your tests mount the component your need to initialise store and pass it into your testing component. Just keep in mind that this would be a brand new instance of Vuex. Here is the code
import { shallowMount } from '#vue/test-utils'
import Vue from 'vue'
import Vuex from 'vuex'
import Tags from '#/components/Tags'
Vue.use(Vuex)
Vue.prototype.$store = new Vuex.Store()
const factory = (propsData) => {
return shallowMount(Tags, {
propsData: {
...propsData
}
})
}
describe('Tags', () => {
it("render tags with passed data", () => {
const wrapper = factory({ loading: true })
// TODO:
})
})
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.
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);
})
})
I'm building up an AppHeader component that connects to a Redux store to get its props. Ideally, I'd probably do it different, but decided to use as an exercise in testing a connected component (I'm new to React).
I'm trying to test this component by using redux-mock-store, but when I get it working in tests, the UI fails. When I get it working in the UI, the tests fail.
The PROBLEM_LINE in AppHeader.component.js below, is where the symptom originates.
When set to appHeader: state.get('AppHeader'), the tests pass successfully, but the console shows:
Uncaught TypeError: state.get is not a function at
Function.mapStateToProps [as mapToProps]`
When set to appHeader: state.AppHeader, the UI correctly displays "Real App Title" inside of an but the test now throws:
TypeError: Cannot read property 'toJS' of undefined
This seems to be an issue with using Immutable.js structures. If I change both initialState variables using plain JS objects, the tests pass and the correct value is displayed. I feel like I must be using immutable incorrectly, or am not getting stores set up correctly with it.
I've read nearly all the posts returned by Google re: testing connected components/containers but most were using state.AppHeader or state.get('AppHeader') in mapStateToProps, and most except a precious few were more on how to hook up redux and react but not so much testing it.
I tried to forego using redux-mock-store, and creating my own store (i.e. a function with dispatch, subscribe, etc) but that didn't solve any problems and only created new ones. Maybe I need to reinvestigate that if that's a better way in the end.
src/index.js
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux'
import {Router, browserHistory} from 'react-router';
import {syncHistoryWithStore} from 'react-router-redux';
import configureStore from './store/configureStore';
import routes from './routes';
const store = configureStore;
const history = syncHistoryWithStore(browserHistory, store);
render(
<Provider store={store}>
<Router history={history} routes={routes} />
</Provider>,
document.getElementById('root')
);
src/store/configureStore.js
import {createStore, combineReducers} from 'redux';
import {routerReducer} from 'react-router-redux';
import * as reducers from '../ducks';
const rootReducer = combineReducers({
routing: routerReducer,
...reducers
});
export default createStore(
rootReducer
);
src/ducks/index.js
import AppHeader from './AppHeader.duck';
export {
AppHeader
};
src/ducks/AppHeader.duck.js
import {fromJS} from 'immutable';
////////////
/// Actions
const SET_APP_TITLE = 'new-react/AppHeader/SET_APP_TITLE';
////////////
/// Reducer
const initialState = fromJS({ <-- USING IMMUTABLE HERE
title: 'Real App Title'
});
export default function reducer(state = initialState, action){
switch(action.type){
case SET_APP_TITLE:
return state.set('title', action.payload.title);
default:
return state;
}
}
////////////
/// Action Creators
export function setAppTitle(title){
return {
type: SET_APP_TITLE,
payload: {title}
};
}
module.exports = reducer;
src/components/AppHeader.component.js
import React from 'react';
import {connect} from 'react-redux';
export class AppHeader extends React.PureComponent {
render() {
const appHeader = this.props.appHeader;
return (
<div className="appHeader">
<h1 className="appHeader_title">
{appHeader.title}
</h1>
</div>
);
}
}
AppHeader.propTypes = {
appHeader: React.PropTypes.object
};
const mapStateToProps = state => {
return {
appHeader: state.AppHeader.toJS() <-- PROBLEM LINE
};
}
export default connect(
mapStateToProps
)(AppHeader);
src/components/AppHeader.component.spec.js
import React from 'react';
import ReactDOM from 'react-dom';
import {mount} from 'enzyme';
import {Provider} from 'react-redux';
import configureMockStore from 'redux-mock-store'
import {fromJS} from 'immutable';
import AppHeader from './AppHeader';
let initialState = fromJS({ <-- USING IMMUTABLE AGAIN
AppHeader: {
title: 'Mock App Title'
}
});
const mockStore = configureMockStore([])(initialState);
describe('AppHeader', () => {
let component;
beforeEach(() => {
const wrapper = mount(
<Provider store={mockStore}>
<AppHeader />
</Provider>
);
component = wrapper.find('AppHeader');
});
it('renders without crashing', () => {
expect(component).toBeDefined();
});
it('shows the app title', () => {
expect(component.find('.appHeader_title').text())
.toBe('App Header Title');
});
});
All dependencies were installed in the last day or so, so are the latest version from npm install. The app itself was created with create-react-app.