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

  • 433