vuex-snapshot
v0.1.5
Published
Take snapshots of your store and actions for Jest snapshot testing
Downloads
18
Maintainers
Readme
vuex-snapshot •
Module to snapshot test vuex actions with jest
Table of contents
Why use snapshot tests for actions?
I hope you are familiar with what jest, vuex and snapshot testing are.
Vuex actions are straightforward to read, and writing tests that are more complex and 10 times longer than the code they cover feels really wrong.
Actions fulfill 3 roles:
- Representation of app logic (conditions & calls of commits\dispatches)
- API for components
- Asynchronous layer for store (as mutations must be sync)
As such we unit test them to make sure that:
- When we change \ add execution path others don't get broken
- Our component API didn't change
vuex-snapshot makes this easy and declarative, even for async actions.
Getting started
Prerequisites
- :heavy_check_mark: Node 6 stable or later
- :heavy_check_mark:
jest
and,babel-jest
installed (es6-modules imports would be used in examples, butvuex-snapshot
is also output as CommonJS)
Installation
via npm
npm install --save-dev vuex-snapshot
via yarn
yarn add --dev vuex-snapshot
Basic example
Say, you are testing some card game
// @/store/actions.js
export const restartGame = ({commit}) => {
commit('shuffleDeck')
commit('setScore', 0)
}
// actions.spec.js
import {snapAction} from 'vuex-snapshot'
import {restartGame} from '@/store/actions'
test('restartGame matches snapshot', () => {
expect(snapAction(restartGame)).toMatchSnapshot()
})
/*
__snapshots__/actions.spec.js
after running jest
*/
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`play restartGame matches snapshot 1`] = `
Array [
Object {
"message": "COMMIT: shuffleDeck",
},
Object {
"message": "COMMIT: setScore",
"payload": 0,
},
]
`;
NOTE: by default vuex-snapshot would not use commit & dispatch from your store, but you can pass them via mocks
Usage
Testing async actions
// @/store/actions.js
export const openDashboard = ({commit, dispatch}) => new Promise((resolve, reject) => {
commit('setRoute', 'loading')
dispatch('load', 'dashboard')
.then(() => {
commit('setRoute', 'dashboard')
resolve()
})
.catch('reject')
})
// actions.spec.js
import {snapAction, MockPromise} from 'vuex-snapshot'
import {openDashboard} from '@/store/actions'
test('openDashboard matches success snapshot', done => {
// MockPromise can be resolved manually unlike default Promise
const dispatch = name => new MockPromise(name)
// order in which promises would be resolved
const resolutions = ['load']
snapAction(openDashboard, {dispatch}, resolutions)
.then(run => {
expect(run).toMatchSnapshot()
done()
})
})
Testing async actions [2]
// @/store/actions.js
export const login = ({commit, dispatch, getters}, creditals) => {
return new Promise((resolve, reject) => {
if(!getters.user.loggedIn) {
fetch('/api/login/', {
method: 'POST',
body: JSON.stringify(creditals)
})
.then(res => res.json())
.then(data => {
commit('setUser', data)
dispatch('setRoute', 'profile')
resolve()
})
.catch(reject)
} else {
resolve()
}
})
}
// actions.spec.js
import {snapAction, useMockFetch, MockPromise} from 'vuex-snapshot'
import {login} from '@/store/actions'
test('login matches success snapshot', done => {
useMockFetch()
const payload = { authCode: 1050 }
const getters = {
user: {
loggedIn: false
}
}
// this is equivalent to calling resolve(payload) inside promise cb
const resolutions = [{
name: '/api/login/',
payload: { json: () => new MockPromise('json') }
}, {
name: 'json',
payload: { name: 'someUser', id: 21 }
}]
snapAction(login, {getters, payload}, resolutions)
.then(run => {
expect(run).toMatchSnapshot()
done()
})
})
// testing error scenarios is just as easy
test('login matches network fail snapshot', done => {
useMockFetch()
const payload = { authCode: 1050 }
const getters = {
user: {
loggedIn: false
}
}
const resolutions = [{
name: '/api/login/',
type: 'reject', // resolve is default value
payload: new TypeError('Failed to fetch')
}]
snapAction(login, {getters, payload}, resolutions)
.then(run => {
/* vuex-snapshot would write that action rejected in the snapshot
so you can test rejections as well */
expect(run).toMatchSnapshot()
done()
})
})
NOTE: promises with same names would be matched to resolutions in order they were created
mocks
By using mocks object you can pass state, getters, payload(action's second argument) of any type,
as well as custom commit
and dispatch
functions.
NOTE: Make sure your getters are what they return, not how they calculate it
Example
const action = jest.fn()
const mocks = {
payload: 0,
state: {
stateValue: 'smth'
},
getters: {
answer: 42
},
commit: console.log
dispatch: jest.fn()
}
snapAction(action, mocks)
// would call the action like
action({
state: mocks.state,
getters: mocks.getters,
commit: (name, payload) => mocks.commit(name, payload, proxies),
dispatch: (name, payload) => mocks.dispatch(name, payload, proxies),
}, mocks.payload)
Proxies is an object with commit and dispatch that were actually passed to action (not those from mocks)
Note: state and getters are being reassigned. Like they would pass
.toEqual
test, but not a.toBe
one.
MockPromises
import {MockPromise} from 'vuex-snapshot'
const name = 'some string'
const cb = (resolve, reject) => {}
new MockPromise(cb, name)
new MockPromise(cb) // name will be 'Promise'
new MockPromise(name) //cb will be () => {}
// some manual control
const toResolve = new MockPromise('some name')
const toReject = new MockPromise('some other name')
const payload = {type: 'any'}
toResolve.resolve(payload)
toReject.reject(payload)
console.log(toReject.name) // some other name
This class extends Promise, so Promise.all and other promise methods work perfectly for it
NOTE:
new MockPromise.then(cb)
actually creates newMockPromise
(that is default Promise behavior). As such there is a risk ofresolutions = ['Promise', 'Promise']
matching this one instead of the Promise you've meant. This is just as true forcatch
,finally
,Promise.all
andPromise.race
snapAction overloads
import {snapAction, Snapshot} from 'vuex-snapshot'
snapAction(action)
snapAction(action, mocks)
snapAction(action, resolutions)
snapAction(action, mocks, resolutions)
snapAction(action, mocks, resolutions, snapshotToWriteTo)
// where snapshotToWriteTo is instance of Snapshot class
If action returned a promise snapAction
would do the same.
That promise will resolve with an Array
of Object
s that represents action's execution.
It could be compared to snapshot, or tested manually.
If vuex-snapshot experienced internal error snapAction test it would reject with an Object
of following structure:
{
err, // Actual error that has been thrown
run // action's execution up to the error point
}
If action returned anything that is not a promise (including undefined
) snapAction
would
synchronously return an array mentioned above.
Utilities
// all vuex-snapshot Utilities
import {
reset,
resetTimetable,
resetConfig,
useMockPromise,
useRealPromise,
useMockFetch,
useRealFetch,
} from 'vuex-snapshot'
reset
Reset calls all other resets and useReal.
resetTimetable
Makes sure no already created promises could be matched to resolutions.
resetConfig
Resets vuexSnapshot.config
to default values.
useMockPromise
Replaces window.Promise
(same as global.Promise
) with vuexSnapshot.MockPromise
that could be named and resolved manually.
useRealPromise
Sets window.Promise
to its original value.
useMockFetch
Replaces window.fetch
(same as global.fetch
) with vuexSnapshot.MockPromise
that could be named and resolved manually.
useRealFetch
Sets window.fetch
to its original value.
Config
These fit very specific types of tests, so using
beforeEach(vuexSnapshot.reset)
is highly encouraged.
vuexSnapshot.config.autoResolve
Default
false
Description
Instead of acting according to passed resolutions vuex-snapshot will automatically trigger resolve on each mock promise in order they were created.
vuexSnapshot.config.snapEnv
Default
false
Description
Starts snapshot with 2 entries:
{
message: 'DATA MOCKS'
payload: {
state //value of state
getters // value of getters
}
}
{
message: 'ACTION MOCKS'
payload // passed action payload if there was one
}
// values of state, gettes and payload are not being copied
vuexSnapshot.config.allowManualActionResolution
Default
false
Description
Allows vuexSnapshot to resolve promise returned by action.
Tips
Mocking timers for vuex-snapshot resolutions
import {snapAction, MockPromise} from 'vuex-snapshot'
test('action snapshot usnig timers', done => {
const realSetTimeout = setTimeout
window.setTimeout = (cb, time) => {
const mock = new MockPromise('Timeout')
mock.then(cb)
return realSetTimeout(mock.resolve, time)
}
// actual "test"
const action = () => new Promise(resolve => {
setTimeout(resolve, 100500)
})
snapAction(action, ['Timeout'])
.then(run => {
expect(run).toMatchSnapshot()
done()
})
.catch(err => {
console.error(err)
console.log(timetable.entries)
done()
})
window.setTimeout = realSetTimeout
})
NOTE: This is not fully accurate simulation because resolving it manually or via resolutions would cause a bit higher priority in event-loop, and resolution on timeout would be 1 tick late Because Promise.then() is not synchronous
Deep testing (execute called actions)
// @/store/actions.js
export const action1 = ({commit, dispatch}) => {
commit('mutation1')
dispatch('action2')
}
export const action2 = ({commit, dispatch}) => {
commit('mutation2')
}
// actions.spec.js
import {snapAction} from 'vuex-snapshot'
import * as actions from '@/store/actions'
test('Many actions', () => {
const state = {}
const getters = {}
const dispatch = (namy, payload, {commit, dispatch}) => {
return actions[name]({state, getters, commit, dispatch}, payload)
}
expect(snapAction(actions.action1, {state, getters, dispatch})).toMatchSnapshot()
})
This should work for async actions too