Router tests with vue-test-utils
What's a router?
It changes the page in a SPA
VueRouter has some fancy things
<router-view> // kinda like jsp include
<router-link :to="path"> // like <a>
HTML
JS
// $router functions
this.$router.push();
this.$router.replace();
// $route properties
this.$route.name;
this.$route.path;
this.$route.query;
Typical test/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VueRouter from 'vue-router'
Vue.use(VueI18n);
Vue.use(Vuex);
Vue.use(VueRouter);
so when your component uses $store, $t, $router, it won't Freak Out™️ in testing
Testing router
// example:
if (this.conditionIsMet) {
this.$router.push({ name: 'somewhere' });
}
describe('push when condition is met', () => {
let vm;
beforeEach(() => {
vm = getComponent();
vm.conditionIsMet = true;
vm.$router.push = sandbox.stub();
});
...
👍
The Problem
what if you need to change $route for testing?
// example:
if (this.$route.name === 'somewhere') {
this.doThis();
} else {
this.doThat();
}
Can't I just..
describe('doThis when route is somewhere', () => {
let vm;
beforeEach(() => {
vm = getComponent();
vm.$route.name = 'somewhere';
});
...
if (this.$route.name === 'somewhere') {
this.doThis();
} else {
this.doThat();
}
Not when Vue.use(Router)....
$route.name is static
🙅♀️
How about...
describe('doThis when route is somewhere', () => {
let vm;
beforeEach(() => {
vm = getComponent();
vm.$router.push({ name: 'somewhere' });
});
...
if (this.$route.name === 'somewhere') {
this.doThis();
} else {
this.doThat();
}
Yes!! but it can get messy.
🤷♀️
The Solution
import Vue from 'vue';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VueRouter from 'vue-router'
Vue.use(VueI18n);
Vue.use(Vuex);
Vue.use(VueRouter);
Why don't I just write my own
Consequences
- No more <router-view>
- No more <router-link>
- no more this.$router
- no more this.$route
Instead of writing your own, use someone else's
- Avoriaz
- vue-test-utils (avoriaz 2.0)
Pros
- Officially sanctioned
- Has great shallow/mount capabilities
- wrapper has .find('query-selector') function so you can easily write visual tests
- good at router testing
Cons
- Still in Beta (as of 4/6/2018)
- If doing router tests, you can't do Vue.use(Router) globally
- You might need to refactor the rendering (mount) parts of your tests
vue-test-utils
Okay, sure... How?
- No more <router-view>
- No more <router-link>
- no more this.$router
- no more this.$route
Let's look at shallow
import { shallow } from '@vue/test-utils'
const wrapper = shallow(Component) // returns a Wrapper containing
// a mounted Component instance
wrapper.vm // the mounted Vue instance
Pass in options to shallow
- context
- slots
- stubs
- mocks
- localVue
- attachToDocument
- attrs
- listeners
- provide
- sync
import { shallow } from '@vue/test-utils'
const wrapper = shallow(Component, {
mocks: {},
stubs: {},
});
router-view, router-link: stubs
import { shallow } from '@vue/test-utils';
import Component from 'src/Component';
const RouterLinkStub = {
name: 'router-link-stub',
render: () => {},
props: ['to'],
};
const RouterViewStub = {
name: 'router-view-stub',
render: () => {},
};
const stubs = {
'router-link': RouterLinkStub,
'router-view': RouterViewStub,
};
const wrapper = shallow(Component, {
stubs,
});
$route, $router: mocks
import { shallow } from '@vue/test-utils';
import Component from 'src/Component';
const mocks = {
$router: {
currentRoute: {},
replace: () => {},
push: () => {},
routes: [], // maybe include your
// app's actual routes here
},
$route: {
path: '/',
hash: '',
params: {},
query: {},
meta: {},
name: null,
fullPath: '/',
from: {},
},
};
const wrapper = shallow(Component, {
mocks,
});
on every file?? 😬😢
// assets.js
export const getSharedAssets = () => {
// stubs
const RouterLinkStub = {
name: 'router-link-stub',
render: () => {},
props: ['to'],
};
const RouterViewStub = {
name: 'router-view-stub',
render: () => {},
};
const stubs = {
'router-link': RouterLinkStub,
'router-view': RouterViewStub,
};
// mocks
const mocks = {
$router: {
currentRoute: {},
replace: () => {},
push: () => {},
routes: [],
},
$route: {
path: '/',
hash: '',
params: {},
query: {},
meta: {},
name: null,
fullPath: '/',
from: {},
},
};
return {
mocks,
stubs,
};
};
// mocks
const mocks = {
...
};
return {
store,
i18n,
mocks,
stubs,
};
};
// assets.js
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import { modules } from 'app/store';
import { i18n } from 'shared/i18n';
export const getSharedAssets = () => {
// store
const store = new Vuex.Store({ modules });
// i18n
const i18n = new VueI18n({
locale: 'en-US',
fallbackLocale: 'en-US',
silentTranslationWarn: true,
missing: () => {},
});
i18n.setLocaleMessage('en-US',
require('static/i18n/en-US.json'));
// stubs
...
add global things: store, i18n
include when mounting
import { shallow } from '@vue/test-utils';
import Component from 'src/Component';
const mocks = {
$router: {
currentRoute: {},
replace: () => {},
push: () => {},
routes: [],
},
$route: {
path: '/',
hash: '',
params: {},
query: {},
meta: {},
name: null,
fullPath: '/',
from: {},
},
};
const RouterLinkStub = {
name: 'router-link-stub',
render: () => {},
props: ['to'],
};
const RouterViewStub = {
name: 'router-view-stub',
render: () => {},
};
const stubs = {
'router-link': RouterLinkStub,
'router-view': RouterViewStub,
};
const getComponent = () => {
const wrapper = shallow(Component, {
mocks,
stubs,
});
return wrapper.vm;
});
import { shallow } from '@vue/test-utils';
import Component from 'src/Component';
import { getSharedAssets } from 'test/assets';
const getComponent = () => {
const wrapper = shallow(Component, {
...getSharedAssets(),
});
return wrapper.vm;
});
Now you can!
describe('doThis when route is somewhere', () => {
let vm;
beforeEach(() => {
vm = getComponent();
vm.$route.name = 'somewhere';
});
...
if (this.$route.name === 'somewhere') {
this.doThis();
} else {
this.doThat();
}
👍
If you really need VueRouter...
you can do that
- context
- slots
- stubs
- mocks
- localVue
- attachToDocument
- attrs
- listeners
- provide
- sync
If you really need VueRouter...
you can do that
import {
shallow,
createLocalVue,
} from '@vue/test-utils'
import VueRouter from 'vue-router'
const localVue = createLocalVue()
localVue.use(VueRouter)
shallow(Component, {
localVue,
})
Questions?
Using vue router in tests
By Laurel Bruggeman
Using vue router in tests
Brief overview of how to test router in vue files, using vue-test-utils
- 511