loading...
Cover image for Writing a Vue component using TDD: a gentle introduction

Writing a Vue component using TDD: a gentle introduction

astagi profile image Andrea Stagi ・7 min read

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 buildscript 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>

Final result

Here you can find the official repo of vue-image-placeholder.

Image by Arno Woestenburg

Posted on Mar 5 by:

astagi profile

Andrea Stagi

@astagi

CTO @ Lotrèk, 🍻hunter by day, FLOSS 👨🏻‍💻, 📸 and 🎸by night, unable to repair your 🖨. Ginger 🍵addicted, 🐱lover.

Discussion

markdown guide
 

I strongly disagree to make a tutorial using packages that are not focued on topic of the post.

Why do you use vue-property-decorator speaking about TDD ?

It's confusing for users to see this new syntax

 

Hi Mirko! Why users should be confused? This tutorial uses TypeScript, if you choose "TypeScript" from vue-cli you'll find vue-property-decorator used in the generated example. vue-property-decorator is not related to TDD, you can replicate all the steps of this tutorial for your Vue component written using JavaScript.