Master Vue Testing: From Unit to End-to-End with Mocha, Cypress, and Vue CLI
Learn how to write comprehensive tests for Vue applications—including unit, integration, and end-to-end tests—using Vue CLI, Mocha, Chai, Vue Test Utils, Vuex, Vue Router, Sinon, and Cypress, with practical code examples that demonstrate test setup, mocking, and browser verification.
1. Types of Tests
There are three main types of tests for Vue applications: unit tests, integration tests, and end‑to‑end (E2E) tests. The testing pyramid illustrates that low‑level unit tests are fast and easy to maintain, while higher‑level integration and E2E tests provide confidence that components work together correctly.
2. Test Runner
When creating a new Vue project, the simplest way to add testing support is to use Vue CLI . During project generation ( vue create myapp) you must manually select unit testing and E2E testing options. After installation, the following dependencies appear in package.json:
@vue/cli-plugin-unit-mocha: Mocha plugin for unit/integration tests
@vue/test-utils: Utility library for unit/integration tests
chai: Assertion libraryTest files use the *.spec.js suffix and reside in the tests/unit directory. You can change the test directory with the --recursive flag:
vue-cli-service test:unit --recursive 'src/**/*.spec.js'3. Unit Tests
Below is a simple unit test that verifies a toUpperCase function:
describe('toUpperCase', () => {
it('should convert string to upper case', () => {
// Arrange
const toUpperCase = info => info.toUpperCase();
// Act
const result = toUpperCase('Click to modify');
// Assert
expect(result).to.eql('CLICK TO MODIFY');
});
});Mocha provides describe and it for structuring tests, while Chai supplies the expect assertions. Most business logic outside component hierarchies—such as state management or backend API handling—should also be covered by unit tests.
4. Component Rendering
Integration tests for components verify both JavaScript logic and DOM rendering. Example component ( Footer.vue) and its test:
<template>
<p class="info">{{ info }}</p>
<button @click="modify">Modify</button>
</template>
<script>
export default {
data: () => ({ info: 'Click to modify' }),
methods: {
modify() { this.info = 'Modified by click'; }
}
};
</script> import { expect } from 'chai';
import { shallowMount } from '@vue/test-utils';
import Footer from '@/components/Footer.vue';
describe('Footer', () => {
it('should render component', () => {
const wrapper = shallowMount(Footer);
const text = wrapper.find('.info').text();
const html = wrapper.find('.info').html();
const classes = wrapper.find('.info').classes();
const element = wrapper.find('.info').element;
expect(text).to.eql('Click to modify');
expect(html).to.eql('<p class="info">Click to modify</p>');
expect(classes).to.eql(['info']);
expect(element).to.be.an.instanceOf(HTMLParagraphElement);
});
});The test uses shallowMount (which does not render child components) or mount for full rendering. Component instances can be accessed via wrapper.vm to call methods or inspect data.
5. Component Interaction
Interaction can be tested by invoking component methods directly:
it('should modify the text after calling modify', () => {
const wrapper = shallowMount(Footer);
wrapper.vm.modify();
expect(wrapper.vm.info).to.eql('Modified by click');
});Or by simulating DOM events:
it('should modify the text after clicking the button', () => {
const wrapper = shallowMount(Footer);
wrapper.find('button').trigger('click');
const text = wrapper.find('.info').text();
expect(text).to.eql('Modified by click');
});6. Parent‑Child Interaction
Parent components communicate with children via props, and children emit events to notify parents. Example child component emitting a modify event and a test that checks emitted events:
export default {
props: ['info'],
methods: {
modify() { this.$emit('modify', 'Modified by click'); }
}
}; it('should handle interactions', () => {
const wrapper = shallowMount(Footer, { propsData: { info: 'Click to modify' } });
wrapper.vm.modify();
expect(wrapper.emitted().modify).to.eql([['Modified by click']]);
});7. Store Integration
Vuex is used for shared state. A simple store with a single info state and a modify mutation can be unit‑tested:
const store = {
state: { info: 'Click to modify' },
mutations: {
modify(state, { info }) { state.info = info; }
}
};
it('should modify state', () => {
const state = {};
store.mutations.modify(state, { info: 'Modified by click' });
expect(state.info).to.eql('Modified by click');
});Integration tests create a local Vue instance, install Vuex, and dispatch actions:
import Vuex from 'vuex';
import { createLocalVue } from '@vue/test-utils';
it('should modify state via action', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
const vuexStore = new Vuex.Store(store);
vuexStore.dispatch('onModify', 'Modified by click');
expect(vuexStore.state.info).to.eql('Modified by click');
});8. Routing
Testing routes requires a local Vue instance with a router plugin. A mock route can be asserted by checking rendered text:
import VueRouter from 'vue-router';
import { createLocalVue } from '@vue/test-utils';
it('should display route', () => {
const localVue = createLocalVue();
localVue.use(VueRouter);
const router = new VueRouter({ routes: [{ path: '*', component: Footer }] });
const wrapper = shallowMount(Footer, { localVue, router });
router.push('/modify');
const text = wrapper.find('.route').text();
expect(text).to.eql('/modify');
});Alternatively, mock the $router object directly:
const wrapper = shallowMount(Footer, {
mocks: { $router: { path: '/modify' } }
});
const text = wrapper.find('.route').text();
expect(text).to.eql('/modify');9. HTTP Requests
External API calls should be mocked. Using Sinon to stub axios.post allows testing of async Vuex actions without real network requests:
import sinon from 'sinon';
import chai from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
it('should set info coming from endpoint', async () => {
const commit = sinon.stub();
sinon.stub(axios, 'post').resolves({ body: 'Modified by post' });
await store.actions.onModify({ commit }, 'Modified by click');
expect(commit).to.have.been.calledWith('modify', { info: 'Modified by post' });
});10. Browser (Cypress)
End‑to‑end tests run in a real browser using Cypress. The following test visits the app, checks initial text, clicks a button, and verifies the updated text:
describe('New todo', () => {
it('it should change info', () => {
cy.visit('/');
cy.contains('.info', 'Click to modify');
cy.get('button').click();
cy.contains('.info', 'Modified by click');
});
});Running Cypress in headless mode adds the --headless flag to the command.
Conclusion
This guide covered the full spectrum of testing a Vue application—from isolated unit tests to full‑stack E2E tests in the browser. By applying these techniques to components, Vuex stores, and routing, developers can catch bugs early and increase confidence in their code.
WecTeam
WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
