cypress-vue-unit-test
A little helper to unit test Vue components in the open source Cypress.io E2E test runner v4.5.0+
Jump to: Comparison, Blog posts, Examples: basic, advanced, full, external, Code coverage
TLDR
- What is this? This package allows you to use Cypress test runner to unit test your Vue components with zero effort.
-
How is this different from vue-test-utils? Vue Test Utils uses Node, requires stubbing browser APIs, and requires users to await Vue's internal event loop. Cypress Vue Unit Test runs each component in the real browser with full power of Cypress E2E test runner: live GUI, full API, screen recording, CI support, cross-platform.
-
If you like using @testing-library/vue
, you can use @testing-library/cypress
for the same findBy
, queryBy
commands, see one of the examples in the list below
Blog posts
Install
Vue CLI Installation
Vue CLI v3+
Recommended: One step install to existing projects with Vue CLI via experimental plugin, read Write Your First Vue Component Test
vue add cypress-experimental
If you want to install this package manually, follow manual install
Usage and Examples
import { mount } from 'cypress-vue-unit-test'
import { HelloWorld } from './HelloWorld.vue'
describe('HelloWorld component', () => {
it('works', () => {
mount(HelloWorld)
cy.contains('Hello World!').should('be.visible')
})
})
Options
You can pass additional styles, css files and external stylesheets to load, see docs/styles.md for full list.
import Todo from './Todo.vue'
const todo = {
id: '123',
title: 'Write more tests',
}
mount(Todo, {
propsData: { todo },
stylesheets: [
'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.css',
],
})
See examples below for details.
Global Vue Options
You can pass extensions (global components, mixins, modules to use)
when mounting Vue component. Use { extensions: { ... }}
object inside
the options
.
The intro example
Take a look at the first Vue v2 example:
Declarative Rendering.
The code is pretty simple
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data() {
return { message: 'Hello Vue!' }
},
})
It shows the message when running in the browser
Hello Vue!
Let's test it in Cypress.io (for the current version see
cypress/integration/spec.js).
import { mountCallback } from 'cypress-vue-unit-test'
describe('Declarative rendering', () => {
const template = `
<div id="app">
{{ message }}
</div>
`
const data = {
message: 'Hello Vue!',
}
beforeEach(mountCallback({ template, data }))
it('shows hello', () => {
cy.contains('Hello Vue!')
})
it('changes message if data changes', () => {
Cypress.vue.message = 'Vue rocks!'
cy.contains('Vue rocks!')
})
})
Fire up Cypress test runner and have real browser (Electron, Chrome) load
Vue and mount your test code and be able to interact with the instance through
the reference Cypress.vue.$data
and via GUI. The full power of the
Cypress API is available.
The list example
There is a list example next in the Vue docs.
<div id="app-4">
<ol>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ol>
</div>
var app4 = new Vue({
el: '#app-4',
data: {
todos: [
{ text: 'Learn JavaScript' },
{ text: 'Learn Vue' },
{ text: 'Build something awesome' },
],
},
})
Let's test it. Simple.
import { mountCallback } from 'cypress-vue-unit-test'
describe('Declarative rendering', () => {
const template = `
<ol>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ol>
`
function data() {
return {
todos: [
{ text: 'Learn JavaScript' },
{ text: 'Learn Vue' },
{ text: 'Build something awesome' },
],
}
}
beforeEach(mountCallback({ template, data }))
it('shows 3 items', () => {
cy.get('li').should('have.length', 3)
})
it('can add an item', () => {
Cypress.vue.todos.push({ text: 'Test using Cypress' })
cy.get('li').should('have.length', 4)
})
})
Handling User Input
The next section in the Vue docs starts with reverse message example.
<div id="app-5">
<p>{{ message }}</p>
<button v-on:click="reverseMessage">Reverse Message</button>
</div>
var app5 = new Vue({
el: '#app-5',
data: {
message: 'Hello Vue.js!',
},
methods: {
reverseMessage: function () {
this.message = this.message.split('').reverse().join('')
},
},
})
We can write the test the same way
import { mountCallback } from 'cypress-vue-unit-test'
describe('Handling User Input', () => {
const template = `
<div>
<p>{{ message }}</p>
<button v-on:click="reverseMessage">Reverse Message</button>
</div>
`
function data() {
return { message: 'Hello Vue.js!' }
}
const methods = {
reverseMessage: function () {
this.message = this.message.split('').reverse().join('')
},
}
beforeEach(mountCallback({ template, data, methods }))
it('reverses text', () => {
cy.contains('Hello Vue')
cy.get('button').click()
cy.contains('!sj.euV olleH')
})
})
Take a look at the video of the test. When you hover over the CLICK
step
the test runner is showing before and after DOM snapshots. Not only that,
the application is fully functioning, you can interact with the application
because it is really running!
Component example
Let us test a complex example. Let us test a single file Vue component. Here is the Hello.vue file
<template>
<p>{{ greeting }} World!</p>
</template>
<script>
export default {
data() {
return {
greeting: 'Hello',
}
},
}
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
note to learn how to load Vue component files in Cypress, see
Bundling section.
Do you want to interact with the component? Go ahead! Do you want
to have multiple components? No problem!
import Hello from '../../components/Hello.vue'
import { mountCallback } from 'cypress-vue-unit-test'
describe('Several components', () => {
const template = `
<div>
<hello></hello>
<hello></hello>
<hello></hello>
</div>
`
const components = {
hello: Hello,
}
beforeEach(mountCallback({ template, components }))
it('greets the world 3 times', () => {
cy.get('p').should('have.length', 3)
})
})
Spying example
Button counter component is used in several Vue doc examples
<template>
<button v-on:click="incrementCounter">{{ counter }}</button>
</template>
<script>
export default {
data() {
return {
counter: 0,
}
},
methods: {
incrementCounter: function () {
this.counter += 1
this.$emit('increment')
},
},
}
</script>
<style scoped>
button {
margin: 5px 10px;
padding: 5px 10px;
border-radius: 3px;
}
</style>
Let us test it - how do we ensure the event is emitted when the button is clicked?
Simple - let us spy on the event, spying and stubbing is built into Cypress
import ButtonCounter from '../../components/ButtonCounter.vue'
import { mountCallback } from 'cypress-vue-unit-test'
describe('ButtonCounter', () => {
beforeEach(mountCallback(ButtonCounter))
it('starts with zero', () => {
cy.contains('button', '0')
})
it('increments the counter on click', () => {
cy.get('button').click().click().click().contains('3')
})
it('emits "increment" event on click', () => {
const spy = cy.spy()
Cypress.vue.$on('increment', spy)
cy.get('button')
.click()
.click()
.then(() => {
expect(spy).to.be.calledTwice
})
})
})
The component is really updating the counter in response to the click
and is emitting an event.
XHR spying and stubbing
The mount function automatically wraps XMLHttpRequest giving you an ability to intercept XHR requests your component might do. For full documentation see Network Requests. In this repo see components/AjaxList.vue and the corresponding tests cypress/integration/ajax-list-spec.js.
created() {
axios.get(`https://jsonplaceholder.cypress.io/users?_limit=3`)
.then(response => {
this.users = response.data
})
}
beforeEach(mountCallback(AjaxList))
it('can inspect real data in XHR', () => {
cy.server()
cy.route('/users?_limit=3').as('users')
cy.wait('@users').its('response.body').should('have.length', 3)
})
it('can display mock XHR response', () => {
cy.server()
const users = [{id: 1, name: 'foo'}]
cy.route('GET', '/users?_limit=3', users).as('users')
cy.get('li').should('have.length', 1)
.first().contains('foo')
})
Spying on window.alert
Calls to window.alert
are automatically recorded, but do not show up. Instead you can spy on them, see AlertMessage.vue and its test cypress/integration/alert-spec.js
Comparison
Feature | Vue Test Utils or @testing-library/vue | Cypress + cypress-vue-unit-test |
---|
Test runs in real browser | ❌ | ✅ |
Uses full mount | ❌ | ✅ |
Test speed | 🏎 | as fast as the app works in the browser |
Test can use additional plugins | maybe | use any Cypress plugin |
Test can interact with component | synthetic limited API | use any Cypress command |
Test can be debugged | via terminal and Node debugger | use browser DevTools |
Built-in time traveling debugger | ❌ | Cypress time traveling debugger |
Re-run tests on file or test change | ✅ | ✅ |
Test output on CI | terminal | terminal, screenshots, videos |
Tests can be run in parallel | ✅ | ✅ via parallelization |
Test against interface | if using @testing-library/vue | ✅ and can use @testing-library/cypress |
Spying and mocking | Jest mocks | Sinon library |
Code coverage | ✅ | ✅ |
Examples
import { mount } from 'cypress-vue-unit-test'
import { HelloWorld } from './HelloWorld.vue'
describe('HelloWorld component', () => {
it('works', () => {
mount(HelloWorld)
cy.contains('Hello World!').should('be.visible')
})
})
Basic examples
Spec | Description |
---|
Components | Registers global components to use |
Filters | Registering global filters |
Hello | Testing examples from Vue2 cookbook |
Mixins | Registering Vue mixins |
Plugins | Loading additional plugins |
Props | Pass props to the component during mount |
Slots | Passing slots and scopedSlots to the component |
Small examples | A few small examples testing forms, buttons |
Advanced examples
Full examples
We have several subfolders in examples folder.
Folder Name | Description |
---|
cli | An example app scaffolded using Vue CLI and the component testing added using vue add cypress-experimental command. |
External examples
Known problems
Bundling
How do we load this Vue file into the testing code? Using webpack preprocessor. Note that this module ships with @cypress/webpack-preprocessor 2.x that requires Webpack 4.x. If you have Webpack 3.x please add @cypress/webpack-preprocessor v1.x
.
Short way
For Webpack Users
Your project probably already has webpack.config.js
setup to transpile
.vue
files. To load these files in the Cypress tests, grab the webpack
processor included in this module, and load it from the cypress/plugins/index.js
file.
const {
onFilePreprocessor,
} = require('cypress-vue-unit-test/preprocessor/webpack')
module.exports = (on) => {
on('file:preprocessor', onFilePreprocessor('../path/to/webpack.config'))
}
Cypress should be able to import .vue
files in the tests
Manual
Using @cypress/webpack-preprocessor and vue-loader.
You can use cypress/plugins/index.js to load .vue
files
using vue-loader
.
const webpack = require('@cypress/webpack-preprocessor')
const webpackOptions = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
],
},
}
const options = {
webpackOptions,
watchOptions: {},
}
module.exports = (on) => {
on('file:preprocessor', webpack(options))
}
Install dev dependencies
npm i -D @cypress/webpack-preprocessor \
vue-loader vue-template-compiler css-loader
And write a test
import Hello from '../../components/Hello.vue'
import { mountCallback } from 'cypress-vue-unit-test'
describe('Hello.vue', () => {
beforeEach(mountCallback(Hello))
it('shows hello', () => {
cy.contains('Hello World!')
})
})
Code coverage
This plugin uses babel-plugin-istanbul
to automatically instrument .js
and .vue
files and generates the code coverage report using dependency cypress-io/code-coverage (included). The output reports are saved in the folder "coverage" at the end of the test run.
If you want to disable code coverage instrumentation and reporting, use --env coverage=false
or CYPRESS_coverage=false
or set in your cypress.json
file
{
"env": {
"coverage": false
}
}
Development
To see all local tests, install dependencies, build the code and open Cypress in GUI mode
npm install
npm run build
npm run cy:open
The build is done using tsc
that transpiles all files from src to dist
folder.
Debugging
Run Cypress with environment variable
DEBUG=cypress-vue-unit-test
If some deeply nested objects are abbreviated and do not print fully, set the maximum logging depth
DEBUG=cypress-vue-unit-test DEBUG_DEPTH=10
FAQ
Related info
Migration guide
From v2 to v3
- update
cypress/plugins/index.js
file to pass the on, config
arguments when creating the default preprocessor. See change, in general the new way is:
const {
onFileDefaultPreprocessor,
} = require('cypress-vue-unit-test/preprocessor/webpack')
module.exports = (on, config) => {
require('@cypress/code-coverage/task')(on, config)
on('file:preprocessor', onFileDefaultPreprocessor(config))
return config
}
Test adapters for other frameworks
Contributors
Small print
Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2017
License: MIT - do anything with the code, but don't blame me if it does not work.
Support: if you find any problems with this module, email / tweet /
open issue on Github
MIT License
Copyright (c) 2017 Gleb Bahmutov <gleb.bahmutov@gmail.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.