• Stars
    star
    104
  • Rank 330,604 (Top 7 %)
  • Language Vue
  • License
    Apache License 2.0
  • Created almost 6 years ago
  • Updated over 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

πŸ”¬ My cheat sheet for testing vue components with jest and vue-test-utils

Vue unit testing cheat sheet

πŸ”¬ My cheat sheet for testing vue components with jest and vue test utils

Remark: In the time of making this cheat sheet, I had no clue about (unit) testing, so these examples might not be in terms of good/best practices.

πŸ™ Many examples in here are taken from the repos, videos and tutorials from MikaelEdebro, Edd Yerburgh, Alex Jover and others.

Useful links:

A few words before

Where is the right balance between what to test and what not to test? We can consider writing unit tests in cases like:

  • when the logic behind the method is complex enough that you feel you need to test extensively to verify that it works.
  • whenever it takes less time to write a unit test to verify that code works than to start up the system, log in, recreate your scenario, etc.
  • when there are possible edge cases of unusually complex code that you think will probably have errors
  • when a particulary complex function is receiving multiple arguments, it is a good idea to feed that function with null, undefined and unexpected data (e.g. methodNullTest, methodInvalidValueTest, methodValidValueTest)
  • when there are cases that require complex steps for reproduction and can be easily forgotten

It's also important to say that we have to tests are inputs and outputs, and NOT the logic between them. Meaning, for example, if we are testing a component that will give us a random number as a result, where we will specify the minimum and maximum number, our inputs will be the lowest number and the highest number (the range) and the output will be our random number. We are not interested in the steps how that result is calculated, just the inputs and outpust. That gives us the flexibility to change or optimize the logic, but the tests should not fail if we do that.

Also, we shouldn't test the framework we are using not the 3rd party libraries. We have to assume they are tested.

Try to keep test methods short and sweet and add them to the build.

Setuping jest

To avoid this chapter, if you are just starting a project or learning, just scafold a new vue project with vue create and while manually selecting features needed for your project, pick unit testing and Jest as your testing framework of choice. Follow these steps for the most basic jest setup:

  1. npm i --save-dev jest

  2. add "unit": "jest" to your package.json file

    {
      "scripts": {
        "unit": "jest",
      },
    } 
  3. create Component.spec.js file in the component folder

  4. add jest: true to your .eslintrc.js file (so that eslint knows the jest keywords like describe, expect etc.)

    {
      env: {
        browser: true,
        jest: true
      },
    }
  5. Write tests in the Component.spec.js file and run it with npm run unit

Things you'll almost always use

  • (1) use mount to mount the component and store it in wrapper
  • (2) access data in a component
  • (3) change a variable in data in a component
  • (4) find an element in a component and check it's properties
  • (5) trigger events like click on an element
  • (6) check if a component has an html element
  • (7) manually pass props to a component

Frequent terminology

  • Shallow Rendering - a technique that assures your component is rendering without children. This is useful for:
    • Testing only the component you want to test (that's what Unit Test stands for)
    • Avoid side effects that children components can have, such as making HTTP calls, calling store actions...

Sanity test

Sanity tests will obviously always have to pass. We are writing them to see that if they somehow fail, we probably didn't set up something right.

describe('Component.vue', () => {
  test('sanity test', () => {
    expect(true).toBe(true) // will obviously always pass
  })
})

Access Vue component

// component access
import { mount } from '@vue/test-utils'
import Modal from '../Modal.vue'
const wrapper = mount(Modal)

wrapper.vm // access to the component instance
wrapper.element // access to the component DOM node

Test Vue components

// Import Vue and the component being tested
import Vue from 'vue'
import MyComponent from 'path/to/MyComponent.vue'

describe('MyComponent', () => {
  // Inspect the raw component options
  it('has a created hook', () => {
    expect(typeof MyComponent.created).toBe('function')
  })

  // access the component data 
  it('check init state of "message" from data', () => {
    const vm = new Vue(MyComponent).$mount()
    expect(vm.message).toBe('bla')
  })
  
  // usage of helper functions 
  it('hides the body initially', () => {
    h.domHasNot('body')
  })
  it('shows body when clicking title', () => {
    h.click('title')
    h.domHas('.body')
  })
  it('renders the correct title and subtitle after clicking button', () => {
    h.click('.my-button')
    h.see('My title')
    h.see('My subtitle')
  })
  it('adds expanded class to expanded post', () => {
    h.click('.post')
    expect(wrapper.classes()).toContain('expanded')
  })
  it('show comments when expanded', () => {
    h.click('.post')
    h.domHas('.comment')
  })
})

Test default and passed props

// we'll create a helper factory function to create a message component, give some properties
const createCmp = propsData => mount(KaModal, { propsData });

it("has a message property", () => {
  cmp = createCmp({ message: "hey" });
  expect(cmp.props().message).toBe("hey");
});

it("has no cat property", () => {
  cmp = createCmp({ cat: "hey" });
  expect(cmp.props().cat).toBeUndefined();
});

// test default value of author prop
it("Paco is the default author", () => {
  cmp = createCmp({ message: "hey" });
  expect(cmp.props().author).toBe("Paco");
});

// slightly different approach
// pass props to child with 'propsData'
it('renders correctly with different props', () => {
  const Constructor = Vue.extend(MyComponent)
  const vm = new Constructor({ propsData: {msg: 'Hello'} }).$mount()
  expect(vm.$el.textContent).toBe('Hello')
})

// prop type
it("hasClose is bool type", () => {
    const message = createCmp({title: "hey"});
    expect(message.vm.$options.props.hasClose.type).toBe(Boolean);
})

// prop required
it("hasClose is not required", () => {
    const message = createCmp({title: "hey"});
    expect(message.vm.$options.props.hasClose.required).toBeFalsy();
})

// custom events
it("Calls handleMessageClick when @message-click happens", () => {
  const stub = jest.fn();
  cmp.setMethods({ handleMessageClick: stub });
  const el = cmp.find(Message).vm.$emit("message-clicked", "cat");
  expect(stub).toBeCalledWith("cat");
});

Test visibility of component with passed prop

test('does not render when not passed visible prop', () => {
  const wrapper = mount(Modal)
  expect(wrapper.isEmpty()).toBe(true)
})
test('render when visibility prop is true', () => {
  const wrapper = mount(Modal, {
    propsData: {
      visible: true
    }
  })
  expect(wrapper.isEmpty()).toBe(false)
})
test('call close() method when X is clicked', () => {
  const close = jest.fn()
  const wrapper = mount(Modal, {
    propsData: {
      visible: true,
      close
    }
  })
  wrapper.find('button').trigger('click')
  expect(close).toHaveBeenCalled()
})

Test computed properties

it("returns the string in normal order", () => {
  cmp.setData({ inputValue: "Yoo" });
  expect(cmp.vm.reversedInput).toBe("Yoo");
});

Vuex actions

Before testing anything from the vuex store, we need to "mock" (make dummy / hardcode) the store values that we want to test. In the case beneath, we simulated the result of the getComments async action to give us 2 comments. Read more https://lmiller1990.github.io/vue-testing-handbook/vuex-actions.html#creating-the-action

import Vuex from 'vuex'
import { shallow, createLocalVue } from '@vue/test-utils'
import BlogComments from '@/components/blog/BlogComments'
import TestHelpers from 'test/test-helpers'
import Loader from '@/components/Loader'
import flushPromises from 'flush-promises'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('BlogComments', () => {
  let wrapper
  let store
  // eslint-disable-next-line
  let h
  let actions
  beforeEach(() => {
    actions = {
      getComments: jest.fn(() => {
        return new Promise(resolve => {
          process.nextTick(() => {
            resolve([{ title: 'title 1' }, { title: 'title 2' }])
          })
        })
      })
    }
    store = new Vuex.Store({
      modules: {
        blog: {
          namespaced: true,
          actions
        }
      }
    })
    wrapper = shallow(BlogComments, {
      localVue,
      store,
      propsData: {
        id: 1
      },
      stubs: {
        Loader
      },
      mocks: {
        $texts: {
          noComments: 'No comments'
        }
      }
    })
    h = new TestHelpers(wrapper, expect)
  })

  it('renders without errors', () => {
    expect(wrapper.isVueInstance()).toBeTruthy()
  })

  it('calls action to get comments on mount', () => {
    expect(actions.getComments).toHaveBeenCalled()
  })

  it('shows loader initially, and hides it when comments have been loaded', async () => {
    h.domHas(Loader)
    await flushPromises()
    h.domHasNot(Loader)
  })

  it('has list of comments', async () => {
    await flushPromises()
    const comments = wrapper.findAll('.comment')
    expect(comments.length).toBe(2)
  })

  it('shows message if there are no comments', async () => {
    await flushPromises()
    wrapper.setData({
      comments: []
    })
    h.domHas('.no-comments')
    h.see('No comments')
  })
})

Vuex mutations and getters

Don't forget to correctly export mutations and getters so they can be accessible in the tests

import { getters, mutations } from '@/store/modules/blog'

describe('blog store module', () => {
  let state
  beforeEach(() => {
    state = {
      blogPosts: []
    }
  })
  describe('getters', () => {
    it('hasBlogPosts logic works', () => {
      expect(getters.hasBlogPosts(state)).toBe(false)
      state.blogPosts = [{}, {}]
      expect(getters.hasBlogPosts(state)).toBe(true)
    })
    it('numberOfPosts returns correct count', () => {
      expect(getters.numberOfPosts(state)).toBe(0)
      state.blogPosts = [{}, {}]
      expect(getters.numberOfPosts(state)).toBe(2)
    })
  })

  describe('mutations', () => {
    it('adds blog posts correctly', () => {
      mutations.saveBlogPosts(state, [{ title: 'New post' }])
      expect(state.blogPosts).toEqual([{ title: 'New post' }])
    })
  })
})

Snapshot test

From the official jest documentation:

Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly. A typical snapshot test case for a mobile app renders a UI component, takes a screenshot, then compares it to a reference image stored alongside the test. The test will fail if the two images do not match: either the change is unexpected, or the screenshot needs to be updated to the new version of the UI component.

You can use this plugin to improve and better control the formatting of your snapshots:

test('Modal renders correctly when visible: true', () => {
  const wrapper = mount(Modal, {
    propsData: {
      visible: true
    }
  });

  // Will store a snapshot of the entire component
  expect(wrapper)
    .toMatchSnapshot();
});
test('Advanced form is shown after clicking "show more"', async () => {
  const wrapper = mount(GenericForm);

  // Target a specific element and similuate interacting with it
  const showMore = wrapper.find('[data-test="showMore"]');
  showMore.trigger('click');
  
  // Wait for the trigger event to be handled
  await wrapper.vm.$nextTick();

  // Will store a snapshot of a portion of the component containing
  // the element that has a matching data-test attribute.
  expect(wrapper.find('[data-test="advancedForm"]'))
    .toMatchSnapshot();
});

Testing helper functions

import { sort } from './helpers'

describe('Helper functions', () => {
  test('it sorts the array', () => {
    const arr = [3,1,5,4,2]
    const res = sort(arr, 'asc')
    expect(res).toEqual(arr.sort((a,b) => a-b))
  })
})

Useful helpers

class TestHelpers {
  constructor(wrapper, expect) {
    this.wrapper = wrapper
    this.expect = expect
  }

  see(text, selector) {
    let wrap = selector ? this.wrapper.find(selector) : this.wrapper
    this.expect(wrap.html()).toContain(text)
  }
  doNotSee(text) {
    this.expect(this.wrapper.html()).not.toContain(text)
  }
  type(text, input) {
    let node = this.find(input)
    node.element.value = text
    node.trigger('input')
  }
  click(selector) {
    this.wrapper.find(selector).trigger('click')
  }
  inputValueIs(text, selector) {
    this.expect(this.find(selector).element.value).toBe(text)
  }
  inputValueIsNot(text, selector) {
    this.expect(this.find(selector).element.value).not.toBe(text)
  }
  domHas(selector) {
    this.expect(this.wrapper.contains(selector)).toBe(true)
  }
  domHasNot(selector) {
    this.expect(this.wrapper.contains(selector)).toBe(false)
  }
  domHasLength(selector, length) {
    this.expect(this.wrapper.findAll(selector).length).toBe(length)
  }
  isVisible(selector) {
    this.expect(this.find(selector).element.style.display).not.toEqual('none')
  }
  isHidden(selector) {
    this.expect(this.find(selector).element.style.display).toEqual('none')
  }
  find(selector) {
    return this.wrapper.find(selector)
  }
  hasAttribute(selector, attribute) {
    return this.expect(this.find(selector).attributes()[attribute]).toBeTruthy()
  }
}

export default TestHelpers
// how to use helpers
import TestHelpers from 'test/test-helpers'
let h = new TestHelpers(wrapper, expect)
h.domHas('.loader')

More Repositories

1

vue-cheat-sheet

πŸ“š My cheat sheet for vue.js most basic stuff https://boussadjra.github.io/vue-cheat-sheet/
Vue
206
star
2

css-cheat-sheet

🎨 My cheat sheet for CSS most basic stuff
CSS
14
star
3

nuxt-cheat-sheet

πŸ“— My cheat sheet for nuxt most basic stuff
Vue
12
star
4

questionator

πŸ™‹ Questionator is meant to provide easier participation and raising questions on meetings, conferences and workshops. Written in Nuxt.js
Vue
11
star
5

naughtypy

πŸ† Get random comment from a random pornhub video
Python
9
star
6

yubikey-guide

πŸ”‘ Simple steps on how to configure Yubico YubiKey 4 on Linux for signed git commits
6
star
7

sly

🐍 Sly.py is a Python script for automatically downloading and organizing key red-teaming and OSCP tools, optimized for use on Kali Linux.
PowerShell
4
star
8

pentest-compass

🧭 My collection of mind maps and diagrams for easier navigation through the complex world of penetration testing
Shell
4
star
9

docker-notes

🐳 My notes created while learning Docker
3
star
10

dekadentno

Sup nerds
3
star
11

helpers.js

πŸš‘ Useful javascript helper functions used in my projects
JavaScript
3
star
12

my-workstation

πŸ’» Configs, plugins, programs I use at my workstation
Shell
2
star
13

blockchain-notes

⛓️ My notes made while learning blockchain
JavaScript
2
star
14

web-accessibility-notes

πŸ‘“ My notes created while learning about web accessibility from various resources.
2
star
15

tetak.gs

Debt collector written in GAS for google spreadsheets
JavaScript
2
star
16

sea_adventures

🐬 Twitter bot that makes tiny sea stories using emojis
Python
1
star
17

reminders

πŸ“ Stuff I use daily but can't seem to remember
Shell
1
star
18

ivanica-motivator

Script that motivates my gf in her studying.
Python
1
star
19

udacity-test-2

JavaScript
1
star
20

beagle.js

🐢 Lightweight scripts for quick checks in web security assessments
JavaScript
1
star
21

dekadentno.github.io

πŸ‘” Personal CV page.
HTML
1
star
22

trashlight

♻️ Notification system for Yeelight color bulb for your weekly garbage pickup schedule
Python
1
star