Writing a Vue component using TDD: a gentle introduction
Andrea Stagi
Posted on March 5, 2020
In this tutorial we'll learn the basic concepts of Test Driven Development (TDD) building a simple Vue component with TypeScript
, testing using Jest
and setting up coverage
and Continuous Integration
.
Introduction
Test Driven Development (TDD) is a development process where you write tests before you write code. You first write a test that describes an expected behaviour and you run it, ensuring it fails, then you write the minimal code to make it pass. After that, if you need, you can refactor the code to make it right. You repeat all these steps for each feature you want to implement until you’re done. This process forces developers to write unit tests and think before writing code, releasing robust code.
It's time to start writing some code to create an image placeholder
component that fetches images from LoremFlickr, a simple service to get random images specifying parameters like width, height, categories (comma separated values), filters.. inside a url, for example to get a 320x240
image from Brazil
or Rio
you can fetch https://loremflickr.com/320/240/brazil,rio
Despite there are a lot of options in LoremFlickr, in this tutorial we'll focus on developing a simple component to get an image from LoremFlickr only using width
and height
and filtering by categories
.
https://loremflickr.com/<width>/<height>/<categories>
Create your project
Using Vue CLI create vue-image-placeholder
project
vue create vue-image-placeholder
Choose Manually select features
and select TypeScript
and Unit testing
options
? Check the features needed for your project:
◉ Babel
◉ TypeScript
◯ Progressive Web App (PWA) Support
◯ Router
◯ Vuex
◯ CSS Pre-processors
◉ Linter / Formatter
◉ Unit Testing
◯ E2E Testing
Use the default settings and select Jest as testing framework.
🧹 Cleanup the project removing assets
, components
folders and App.vue
inside src
, we don't need them for this tutorial.
Write your first test
In tests/unit
rename example.spec.ts
with imageplaceholder.spec.ts
and start writing your first test.
We expect our ImagePlaceholder
component to render an <img>
tag with src
composed by width
, height
and images
(categories) properties.
<ImagePlaceholder width=500 height=250 images="dog" />
Should render
<img src="https://loremflickr.com/500/250/dog">
Let's write our first test to check if ImagePlaceholder
component with properties width: 500
, height:200
, images: 'newyork'
renders an img
with src=https://loremflickr.com/500/200/newyork
.
import { shallowMount } from '@vue/test-utils'
import ImagePlaceholder from '@/ImagePlaceholder.vue'
describe('ImagePlaceholder.vue', () => {
it('renders the correct url for New York images', () => {
const wrapper = shallowMount(ImagePlaceholder, {
propsData: { width: 500, height:200, images: 'newyork' }
})
expect(
wrapper.findAll('img').at(0).attributes().src
).toEqual('https://loremflickr.com/500/200/newyork')
})
})
If we try to run tests with
yarn test:unit
❌ Everything fails as expected, because ImagePlaceholder
component does not exist.
To make tests pass you need to write the component ImagePlaceholder.vue
<template>
<img :src="url">
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class ImagePlaceholder extends Vue {
@Prop({required: true}) readonly width!: number
@Prop({required: true}) readonly height!: number
@Prop({required: true}) readonly images!: string
get url() {
return `https://loremflickr.com/${this.width}/${this.height}/${this.images}`;
}
}
</script>
Save the file and run yarn test:unit
again.
yarn run v1.19.2
$ vue-cli-service test:unit
PASS tests/unit/imageplaceholder.spec.ts
ImagePlaceholder.vue
✓ renders the correct url for New York images (46ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.428s
Ran all test suites.
✨ Done in 2.40s.
✅ Yay! Tests run without errors!
You've just created a minimal ImagePlaceholder
component using TDD!
See it in action: copy and paste the following code in main.ts
import Vue from 'vue'
import ImagePlaceholder from './ImagePlaceholder.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(
ImagePlaceholder,
{
props : {
width: 500,
height:200,
images: 'newyork'
}
}),
}).$mount('#app')
and run yarn serve
!
Improve the component using TDD
Suppose you want to add a new feature to ImagePlaceholder
component: use "random"
category if images
prop is not specified. With this feature
<ImagePlaceholder width=500 height=200 />
should render
<img src="https://loremflickr.com/500/200/random">
This is the behaviour expected in the following test
it('renders the correct url for Random images if not specified', () => {
const wrapper = shallowMount(ImagePlaceholder, {
propsData: { width: 500, height:200 }
})
expect(
wrapper.findAll('img').at(0).attributes().src
).toEqual('https://loremflickr.com/500/200/random')
})
❌ After running yarn test:unit
you will get this error
● ImagePlaceholder.vue › renders the correct url for Random images if not specified
expect(received).toEqual(expected) // deep equality
Expected: "https://loremflickr.com/500/200/random"
Received: "https://loremflickr.com/500/200/undefined"
Following TDD, it's time to write some code again to make tests passing: now images
prop should not be required anymore and "random"
should be its default value.
//...
@Prop({required: false, default: 'random'}) readonly images!: string
//...
✅ Run tests again and they will pass as expected!
What about support square images and make height
equal to width
if not specified? Again write a failing test
it('renders a square image if height is not specified', () => {
const wrapper = shallowMount(ImagePlaceholder, {
propsData: { width: 500 }
})
expect(
wrapper.findAll('img').at(0).attributes().src
).toEqual('https://loremflickr.com/500/500/random')
})
And write the minimal code to make it pass.
@Component
export default class ImagePlaceholder extends Vue {
@Prop({required: true}) readonly width!: number
@Prop({required: false}) readonly height!: number
@Prop({required: false, default: 'random'}) readonly images!: string
get url(): string {
let height = this.height;
if (!this.height) {
height = this.width;
}
return `https://loremflickr.com/${this.width}/${height}/${this.images}`
}
}
✅ Tests pass!
There's a test for this new feature, and the minimal code to make it passes. We can make some refactoring! 👨🏻💻
export default class ImagePlaceholder extends Vue {
@Prop({required: true}) readonly width!: number
@Prop({required: false}) readonly height!: number
@Prop({required: false, default: 'random'}) readonly images!: string
get url(): string {
return `https://loremflickr.com/${this.width}/${this.height || this.width}/${this.images}`;
}
}
✅ Tests pass again! We’ve successfully refactored the code without affecting the output!
Iterate this process to implement anything you want! Remember: think about what you want, write a test first, make it fail and write the minimal code to make it pass! Then refactor your code if you need.
You can find the complete code on GitHub
Add code coverage
Code coverage is a measurement of how many lines, branches, statements of your code are executed while the automated tests are running. Apps with a high percentage of code covered has a lower chance of containing undetected bugs compared to apps with low test coverage.
Jest can generate code coverage easily without external tools. To enable this feature add some lines to jest.config.json
file specifying which files will be covered
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
collectCoverage: true,
collectCoverageFrom: ["src/**/*.vue", "!**/node_modules/**"]
}
Run again yarn test:unit
and you'll get the coverage report before testing results.
----------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
ImagePlaceholder.vue | 100 | 100 | 100 | 100 | |
----------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 5.688s
Ran all test suites.
✨ Done in 8.70s.
⚠️ Remember to add /coverage
folder generated by Jest to .gitignore
.
Continuous Integration
Continuous Integration (CI) is a development practice where developers integrate code into a shared repository frequently, preferably several times a day. Each integration can then be verified by an automated build and automated tests. The goal is to build healthier software by developing and testing in smaller increments. This is where a continuous integration platform like TravisCI comes in.
We need also another useful service, Codecov, to monitor code coverage percentage.
TravisCI and Codecov are integrated with Github, you just need to signup and add the project to the services. Inside your code you need a special file, .travis.yml
to activate CI and say to TravisCI how to execute builds:
language: node_js
node_js:
- 10
before_script:
- yarn add codecov
script:
- yarn test:unit
after_script:
codecov
Following these steps TravisCI will
- setup the environment (
node_js 10
) - install dependencies (
before_script
section) - execute tests with coverage (
script
section) - send coverage report to Codecov(
after_script
section)
Setup build
Now that we have our component ready, we need to setup the build process. In your package.json
file modify the build
script and remove the serve
script.
"scripts": {
"build": "vue-cli-service build --target lib --name vue-image-placeholder src/main.ts",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
With --target lib
file main.ts
must be changed accordingly to export your component
import ImagePlaceholder from './ImagePlaceholder.vue'
export default ImagePlaceholder
Add a folder types
with a file called index.d.ts
inside, containing
declare module 'vue-image-placeholder' {
const placeholder: any;
export default placeholder;
}
Add main
and typings
references to package.json
"main": "./dist/vue-image-placeholder.common.js",
"typings": "types/index.d.ts",
You need also to disable automatic polyfill injection in babel.config.js
module.exports = {
presets: [
['@vue/app', {
useBuiltIns: false
}]
]
}
And remove test files from "include"
section of tsconfig.json
.
To build the library for production run
yarn build
⠦ Building for production as library (commonjs,umd,umd-min)...
DONE Compiled successfully in 20857ms 11:37:47 PM
File Size Gzipped
dist/vue-image-placeholder.umd.min.js 8.50 KiB 3.16 KiB
dist/vue-image-placeholder.umd.js 42.33 KiB 11.76 KiB
dist/vue-image-placeholder.common.js 41.84 KiB 11.60 KiB
📦 The build is ready!
To play with it, install vue-image-placeholder
in other apps locally using
yarn add ../vue-image-placeholder
and use the component
<template>
<div id="app">
<h1>Welcome to the Vue Image Placeholder demo!</h1>
<ImagePlaceholder width=500 />
</div>
</template>
<script>
import ImagePlaceholder from 'vue-image-placeholder';
export default {
name: 'App',
components: {
ImagePlaceholder
}
}
</script>
✨ Here you can find the official repo of vue-image-placeholder
.
Image by Arno Woestenburg
Posted on March 5, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.