Writing tests for Vue.js Storybook
Tags: storybook, vue, testing, jest
Published: 2019-10-19 22:46:50

---
title: Writing tests for Vue.js Storybook
published: true
description: How to install Jest to your Storybook project with examples
tags: storybook, vue.js, testing, jest
cover_image: https://thepracticaldev.s3.amazonaws.com/i/ijofno0kloo5tcoa8xeb.png
---

Over the last couple of weeks, I've found new joy with writing my Vue.js components within Storybook as a tool to visualise all the possible permutations of a given Component in isolation from the target application. 

It's all fair game writing your code, hitting save and seeing the change in the browser and visually observing everything works as expected.  That's not good enough! I want unit-tests to ensure my components functionality is what I expect. ✅

In this guide, I'll show you how to install Jest to your Storybook project and examples of tests for Vue.js components. 

# Getting started
If you already have Storybook and Vue.js installed to your project, please skip to [Installing Jest](#installing-jest).

Let's get you quickly started with Storybook and Vue.js by creating a new project folder where your stories will reside. 

Make a new folder; here we'll call it `design-system` but you can call it whatever you like. 

```bash
mk ./design-system
cd ./design-system
```

Now we'll install our main dependencies Vue.js and Storybook.

_note: My personal preference is the Single File Component style of Vue.js for ease of understanding between projects._

```bash
npm init -y # initialize a new package.json quicly
npm install --save vue
npm install --save-dev vue-loader vue-template-compiler @babel/core babel-core@^7.0.0-bridge.0 babel-loader babel-preset-vue
npx -p @storybook/cli sb init --type sfc_vue
```

Hooray! We've got Storybook installed with a couple of Vue.js examples to start.

Let's boot the Storybook server and see what we got.

```bash
npm run storybook
```

![Screenshot 2019-10-19 20.50.02.png](https://ucarecdn.com/9960a661-c0c3-4903-af50-e9b10015e837/)

That is great and all, but now we'll want to set up Jest. 😄



# <a name="installing-jest"/>Installing Jest

Let's get stuck right in and install all the dependencies required.

```bash
npm install --save-dev jest vue-jest babel-jest @babel/core @babel/preset-env @vue/test-utils
```

Configure Babel by creating a `babel.config.js` file in the root of the project.

```javascript
// babel.config.js
module.exports = {
  presets: [
    '@babel/preset-env'
  ]
}

```

Configuration for Jest will need to be added too by creating a `jest.config.js` file in the root of the project.

```javascript
// jest.config.js
module.exports = {
  moduleFileExtensions: ['js', 'vue', 'json'],
  transform: {
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest'
  },
  collectCoverage: true,
  collectCoverageFrom: ['<rootDir>/src/**/*.vue'],
  transformIgnorePatterns: ["/node_modules/(?!@babel/runtime)"],
  coverageReporters: ["text-summary", "html", "lcov", "clover"]
}
```

Finally, we'll need to update the `package.json` scripts to reference Jest as our test runner. 

```json
// package.json
{
  "name": "storybook-vue",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "jest",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  },
  ...
}
```

Before we continue, let's give our installation a quick run to ensure everything is looking ok. 

We'll have to run Jest with `--passWithNoTests` as we haven't written any tests yet.

_note: the double dashes ` -- ` on their own are intentional to allow the arguments to be passed through to the inner command._

```bash
npm run test -- --passWithNoTests
```

We should see the following output.

```bash
npm run test -- --passWithNoTests

> storybook-vue@1.0.0 test ~/code/design-system
> jest "--passWithNoTests"

No tests found, exiting with code 0

=============================== Coverage summary ===============================
Statements   : Unknown% ( 0/0 )
Branches     : Unknown% ( 0/0 )
Functions    : Unknown% ( 0/0 )
Lines        : Unknown% ( 0/0 )
================================================================================
```

Great!, everything looks like it's wired up ok for Jest to be happy, now let's write some tests. 🤖

# Writing our first test

Given we set up the project fresh and ran the initialise command in Storybook, we should have some simple example stories waiting for us in `src/stories`.

For example, our project structure would look something like this. 

```bash
tree -I 'node_modules|coverage'
.
|-- babel.config.js
|-- jest.config.js
|-- package-lock.json
|-- package.json
`-- src
    `-- stories
        |-- 0-Welcome.stories.js
        |-- 1-Button.stories.js
        |-- MyButton.vue
        `-- Welcome.vue

2 directories, 8 files
```

Create a new file in the `src/stories` directory called `MyButton.test.js` so we can write our first tests for `MyButton.vue`.

In this test file, we'll import the `MyButton.vue` component and `@vue/test-utils`.

```javascript
// src/stories/MyButton.test.js
import Component from './MyButton.vue';
import { shallowMount } from "@vue/test-utils";

describe('MyButton', () => {
  let vm
  let wrapper
  beforeEach(() => {
    wrapper = shallowMount(Component)
    vm = wrapper.vm
  })
})
```

Looking at our `MyButton.vue` file, we'll see in the `<script>` block a method called `onClick`.  

```javascript
// src/stories/MyButton.vue (fragment)
export default {
  name: 'my-button',

  methods: {
    onClick () {
      this.$emit('click');
    }
  }
}
```

This method, when called, will emit a `click` event to any parent consuming components.  So testing this will require us to spy on `$emit`, and we will expect `$emit` to be called with `click`.

Our test will look like the following.

```javascript
// src/stories/MyButton.test.js (fragment)
describe('onClick', () => {
  it('emits click', () => {
    vm.$emit = jest.fn()
    vm.onClick()
    expect(vm.$emit).toHaveBeenCalledWith('click')
  })
})
```

Here's a full example of our `MyButton.vue.js` test file. 

```javascript
// src/stories/MyButton.test.js
import { shallowMount } from "@vue/test-utils";
import Component from './MyButton.vue';

describe('MyButton', () => {
  let vm
  let wrapper
  beforeEach(() => {
    wrapper = shallowMount(Component)
    vm = wrapper.vm
  })

  describe('onClick', () => {
    it('emits click', () => {
      vm.$emit = jest.fn()
      vm.onClick()
      expect(vm.$emit).toHaveBeenCalledWith('click')
    })
  })
})
```

Brilliant! We can run our tests and see how we're doing.

```bash
npm run test

> storybook-vue@1.0.0 test ~/code/design-system
> jest

 PASS  src/stories/MyButton.test.js
  MyButton
    onClick
      ✓ emits click (15ms)


=============================== Coverage summary ===============================
Statements   : 25% ( 1/4 )
Branches     : 100% ( 0/0 )
Functions    : 33.33% ( 1/3 )
Lines        : 25% ( 1/4 )
================================================================================
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.921s
Ran all test suites.
```

🎉 Congratulations you've just written our first test for our Storybook project!

... but what is that in the Coverage summary? 25% of the lines are covered? That has to be improved.

# Improving code coverage


As we did with our first test, we'll create a new file for the other component `Welcome.test.js` in the `src/stories` directory. 

The contents of `Welcome.vue` is a little more involved with props and having to `preventDefault`. 

```javascript
// src/stories/Welcome.vue
const log = () => console.log('Welcome to storybook!')

export default {
  name: 'welcome',

  props: {
    showApp: {
      type: Function,
      default: log
    }
  },

  methods: {
    onClick (event) {
      event.preventDefault()
      this.showApp()
    }
  }
}
```

Let's cover the natural part first, `methods` as with the tests in `MyButton.test.js` we can copy most of this code across. 

As our code stipulates, we'll need to spy on the given property `showApp` to ensure it is called and the event we provide will have to include `preventDefault`.

```javascript
// src/stories/Welcome.test.js (fragment)
describe('onClick', () => {
  it('calls showApp', () => {
    let showApp = jest.fn()
    wrapper.setProps({
      showApp
    })
    let event = {
      preventDefault: jest.fn()
    }
    vm.onClick(event)
    expect(showApp).toHaveBeenCalled()
    expect(event.preventDefault).toHaveBeenCalled()
  })
})
```

Testing props have a subtle difference to it as we need to fully mount the component to access the `$options` where `props` are defined. 


```javascript
// src/stories/Welcome.test.js (fragment)
describe("props.showApp", () => {
  it('logs message', () => {
    wrapper = mount(Component)
    vm = wrapper.vm
    let prop = vm.$options.props.showApp;

    let spy = jest.spyOn(console, 'log').mockImplementation()
    prop.default()
    expect(console.log).toHaveBeenCalledWith('Welcome to storybook!')
    spy.mockRestore()
  })
})
```

Ensure to import `mount` from `@vue/test-utils`

```javascript
// src/stories/Welcome.test.js (fragment)
import { shallowMount, mount } from "@vue/test-utils";
```

You would notice we're using `jest.spyOn()` to mock the implementation of `console.log` to allow us to assert `.toHaveBeCalledWith` and then restore the `console.log` to its initial application once our test has completed. 


Here is a full example of the test file.

```javascript
// src/stories/Welcome.test.js
import { shallowMount, mount } from "@vue/test-utils";
import Component from './Welcome.vue';

describe('Welcome', () => {
  let vm
  let wrapper
  beforeEach(() => {
    wrapper = shallowMount(Component)
    vm = wrapper.vm
  })

  describe("props.showApp", () => {
    it('logs message', () => {
      wrapper = mount(Component)
      vm = wrapper.vm
      let prop = vm.$options.props.showApp;

      let spy = jest.spyOn(console, 'log').mockImplementation()
      prop.default()
      expect(console.log).toHaveBeenCalledWith('Welcome to storybook!')
      spy.mockRestore()
    })
  })

  describe('onClick', () => {
    it('calls showApp', () => {
      let showApp = jest.fn()
      wrapper.setProps({
        showApp
      })
      let event = {
        preventDefault: jest.fn()
      }
      vm.onClick(event)
      expect(showApp).toHaveBeenCalled()
      expect(event.preventDefault).toHaveBeenCalled()
    })
  })
})
```

We can rerun our tests and fingers crossed the coverage should be vastly improved. 🤞

```bash
npm test

> storybook-vue@1.0.0 test ~/code/design-system
> jest

 PASS  src/stories/MyButton.test.js
 PASS  src/stories/Welcome.test.js

=============================== Coverage summary ===============================
Statements   : 100% ( 4/4 )
Branches     : 100% ( 0/0 )
Functions    : 100% ( 3/3 )
Lines        : 100% ( 4/4 )
================================================================================

Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        2.404s
Ran all test suites.
```

That is Awesome, well done! 🚀


# Notes

With most code challenges, I usually battle through small problems along the way. Here I like to give credit to where I have found solutions to the issues I have experienced while getting the project setup.

Using Jest with Babel [as documented](https://jestjs.io/docs/en/getting-started#using-babel) required adding `babel-core@7.0.0-bridge.0` to the development dependencies to ensure it works well with Babel 7.

You'll notice in the `jest.config.js` I included a `transformIgnorePatterns` definition. Although the current code doesn't demand too much from Core.js, I added this definition. It will save some headake later on in your development, avoiding the no descriptive `SyntaxError: Unexpected identifier` issues. 

Thank you for reading, I hope this helped you get your Vue.js Storybook project to the next level.
🙏