Writing a Vue component using TDD: a gentle introduction
Tags: vue, typescript, tdd, tutorial
Published: 2020-03-05 16:44:27

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](https://loremflickr.com/), 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](https://loremflickr.com/320/240/brazil,rio)

<img src="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](https://cli.vuejs.org/) 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](https://jestjs.io) 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. 

```html
<ImagePlaceholder width=500 height=250 images="dog" />
```

Should render

```html
<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`.

```js
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

```sh
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`

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

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

```html
<ImagePlaceholder width=500 height=200 />
```

should render

```html
<img src="https://loremflickr.com/500/200/random">
```

This is the behaviour expected in the following test

```js
  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.

```ts
  //...
  @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

```ts
  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.

```ts
@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! 👨🏻‍💻

```ts
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](https://github.com/astagi/tdd-with-vue)

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

```js
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](https://travis-ci.org) comes in.

We need also another useful service, [Codecov](https://codecov.io), 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:

```yaml
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.

```js
  "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

```ts
import ImagePlaceholder from './ImagePlaceholder.vue'

export default ImagePlaceholder
```

Add a folder `types` with a file called `index.d.ts` inside, containing

```ts
declare module 'vue-image-placeholder' {
  const placeholder: any;
  export default placeholder;
}
```

Add `main` and `typings` references to `package.json`

```js
  "main": "./dist/vue-image-placeholder.common.js",
  "typings": "types/index.d.ts",
```

You need also to [disable automatic polyfill injection](https://cli.vuejs.org/guide/browser-compatibility.html#polyfills-when-building-as-library-or-web-components) in `babel.config.js`

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

```sh
⠦  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

```sh
yarn add ../vue-image-placeholder
```

and use the component

```Vue
<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>
```

<img alt="Final result" src="https://github.com/astagi/tdd-with-vue/blob/master/assets/screen.png?raw=true">


✨ [Here](https://github.com/astagi/vue-image-placeholder) you can find the official repo of `vue-image-placeholder`.

Image by [Arno Woestenburg](https://nl.linkedin.com/in/arno-woestenburg-767573148)