VueJS: Guides

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Cài đặt

Để nhanh chóng sử dụng Vue Test Utils, hãy sao chép kho lưu trữ demo của chúng tôi với thiết lập cơ bản và cài đặt các phụ thuộc:

git clone https://github.com/vuejs/vue-test-utils-getting-started
cd vue-test-utils-getting-started
npm install

Bạn sẽ thấy rằng dự án bao gồm một thành phần đơn giản , counter.js:

// counter.js

export default {
  template: `
    <div>
      <span class="count">{{ count }}</span>
      <button @click="increment">Increment</button>
    </div>
  `,

  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}

#Thành phần lắp

Vue Test Utils kiểm tra các thành phần Vue bằng cách gắn chúng cách ly, chế nhạo các đầu vào cần thiết (đạo cụ, tiêm và sự kiện người dùng) và xác nhận đầu ra (kết quả hiển thị, phát ra các sự kiện tùy chỉnh).

Các thành phần được gắn kết được trả về bên trong Wrapper , hiển thị nhiều phương thức thuận tiện để thao tác, duyệt và truy vấn đối tượng thành phần Vue bên dưới.

Bạn có thể tạo các hàm bao bằng mountphương thức. Hãy tạo một tệp có tên test.js:

// test.js

// Import the `mount()` method from the test utils
// and the component you want to test
import { mount } from '@vue/test-utils'
import Counter from './counter'

// Now mount the component and you have the wrapper
const wrapper = mount(Counter)

// You can access the actual Vue instance via `wrapper.vm`
const vm = wrapper.vm

// To inspect the wrapper deeper just log it to the console
// and your adventure with the Vue Test Utils begins
console.log(wrapper)

#Kiểm tra kết xuất HTML của thành phần

Bây giờ chúng ta có trình bao bọc, điều đầu tiên chúng ta có thể làm là xác minh rằng đầu ra HTML được kết xuất của thành phần khớp với những gì được mong đợi.

import { mount } from '@vue/test-utils'
import Counter from './counter'

describe('Counter', () => {
  // Now mount the component and you have the wrapper
  const wrapper = mount(Counter)

  it('renders the correct markup', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // it's also easy to check for the existence of elements
  it('has a button', () => {
    expect(wrapper.contains('button')).toBe(true)
  })
})

Bây giờ chạy thử nghiệm với npm test. Bạn sẽ thấy các bài kiểm tra đi qua.

#Mô phỏng tương tác người dùng

Bộ đếm của chúng tôi sẽ tăng số lượng khi người dùng nhấp vào nút. Để mô phỏng hành vi, trước tiên chúng ta cần xác định vị trí nút wrapper.find(), trong đó trả về một trình bao bọc cho phần tử nút . Sau đó chúng ta có thể mô phỏng nhấp chuột bằng cách gọi .trigger()trình bao bọc nút:

it('button click should increment the count', () => {
  expect(wrapper.vm.count).toBe(0)
  const button = wrapper.find('button')
  button.trigger('click')
  expect(wrapper.vm.count).toBe(1)
})

#Thế còn nextTick?

Các lô Vue đang chờ cập nhật DOM và áp dụng chúng không đồng bộ để ngăn kết xuất lại không cần thiết do nhiều đột biến dữ liệu gây ra. Đây là lý do tại sao trong thực tế, chúng ta thường phải sử dụng Vue.nextTickđể đợi cho đến khi Vue thực hiện cập nhật DOM thực tế sau khi chúng tôi kích hoạt một số thay đổi trạng thái.

Để đơn giản hóa việc sử dụng, Vue Test Utils áp dụng đồng bộ tất cả các bản cập nhật, do đó bạn không cần sử dụng Vue.nextTickđể chờ cập nhật DOM trong các thử nghiệm của mình.

Lưu ý: nextTickvẫn cần thiết khi bạn cần tiến hành vòng lặp sự kiện một cách bùng nổ, cho các hoạt động như gọi lại không đồng bộ hoặc giải quyết lời hứa.

Nếu bạn vẫn cần phải sử dụng nextTicktrong các tệp thử nghiệm của mình, hãy lưu ý rằng mọi lỗi được ném bên trong nó có thể không bị người chạy thử của bạn bắt gặp vì nó sử dụng các lời hứa trong nội bộ. Có hai cách để khắc phục điều này: hoặc bạn có thể đặt cuộc donegọi lại là trình xử lý lỗi toàn cầu của Vue khi bắt đầu kiểm tra hoặc bạn có thể gọi nextTickmà không cần đối số và trả lại như một lời hứa:

// this will not be caught
it('will time out', (done) => {
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

// the two following tests will work as expected
it('will catch the error using done', (done) => {
  Vue.config.errorHandler = done
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

it('will catch the error using a promise', () => {
  return Vue.nextTick()
    .then(function () {
      expect(true).toBe(false)
    })
})

#Cái gì tiếp theo

#Lời khuyên phổ biến

#Biết những gì cần kiểm tra

Đối với các thành phần UI, chúng tôi không khuyên bạn nên nhắm đến phạm vi bảo hiểm hoàn toàn dựa trên dòng, vì nó dẫn đến quá nhiều sự tập trung vào các chi tiết triển khai bên trong của các thành phần và có thể dẫn đến các thử nghiệm dễ vỡ.

Thay vào đó, chúng tôi khuyên bạn nên viết các bài kiểm tra khẳng định giao diện chung của thành phần của bạn và coi phần bên trong của nó là một hộp đen. Một trường hợp thử nghiệm duy nhất sẽ khẳng định rằng một số đầu vào (tương tác người dùng hoặc thay đổi đạo cụ) được cung cấp cho kết quả thành phần trong đầu ra dự kiến ​​(kết quả hiển thị hoặc phát ra các sự kiện tùy chỉnh).

Ví dụ: đối với Counterthành phần tăng bộ đếm hiển thị lên 1 mỗi lần nhấp vào nút, trường hợp thử nghiệm của nó sẽ mô phỏng nhấp chuột và khẳng định rằng đầu ra được hiển thị đã tăng thêm 1. Thử nghiệm không quan tâm đến việc Countertăng giá trị như thế nào , nó chỉ quan tâm đến đầu vào và đầu ra.

Lợi ích của phương pháp này là miễn là giao diện chung của thành phần của bạn vẫn giữ nguyên, các thử nghiệm của bạn sẽ vượt qua bất kể việc triển khai nội bộ của thành phần thay đổi theo thời gian như thế nào.

Chủ đề này được thảo luận với nhiều chi tiết hơn trong một bài thuyết trình tuyệt vời của Matt O'Connell .

# Kết xuấtnông

Trong các thử nghiệm đơn vị, chúng tôi thường muốn tập trung vào thành phần đang được thử nghiệm như một đơn vị độc lập và tránh gián tiếp khẳng định hành vi của các thành phần con của nó.

Ngoài ra, đối với các thành phần có chứa nhiều thành phần con, toàn bộ cây được kết xuất có thể trở nên thực sự lớn. Lặp đi lặp lại tất cả các thành phần con có thể làm chậm các bài kiểm tra của chúng tôi.

Vue Test Utils cho phép bạn gắn kết một thành phần mà không hiển thị các thành phần con của nó (bằng cách khai thác chúng) bằng shallowMountphương thức:

import { shallowMount } from '@vue/test-utils'

const wrapper = shallowMount(Component)
wrapper.vm // the mounted Vue instance

#Khẳng định sự kiện phát ra

Mỗi trình bao bọc được gắn tự động ghi lại tất cả các sự kiện được phát ra bởi thể hiện Vue bên dưới. Bạn có thể truy xuất các sự kiện đã ghi bằng wrapper.emitted()phương thức:

wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)

/*
`wrapper.emitted()` returns the following object:
{
  foo: [[], [123]]
}
*/

Sau đó, bạn có thể xác nhận dựa trên những dữ liệu này:

// assert event has been emitted
expect(wrapper.emitted().foo).toBeTruthy()

// assert event count
expect(wrapper.emitted().foo.length).toBe(2)

// assert event payload
expect(wrapper.emitted().foo[1]).toEqual([123])

Bạn cũng có thể nhận được một mảng các sự kiện theo thứ tự phát ra của chúng bằng cách gọi wrapper.emittedByOrder().

#Phát ra sự kiện từ Thành phần trẻ em

Bạn có thể phát ra một sự kiện tùy chỉnh từ một thành phần con bằng cách truy cập thể hiện.

Thành phần được thử nghiệm

<template>
  <div>
    <child-component @custom="onCustom"/>
    <p v-if="emitted">Emitted!</p>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'

export default {
    name: 'ParentComponent',
    components: { ChildComponent },
    data() {
        return {
            emitted: false
        }
    },
    methods: {
        onCustom () {
            this.emitted = true
        }
    }
}
</script>

Kiểm tra

import { shallowMount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent'
import ChildComponent from '@/components/ChildComponent'

describe('ParentComponent', () => {
  it("displays 'Emitted!' when custom event is emitted", () => {
    const wrapper = shallowMount(ParentComponent)
    wrapper.find(ChildComponent).vm.$emit('custom')
    expect(wrapper.html()).toContain('Emitted!')
  })
})

#Thao tác thành phần nhà nước

Bạn có thể trực tiếp thao tác trạng thái của thành phần bằng cách sử dụng setDatahoặc setPropsphương thức trên trình bao bọc:

wrapper.setData({ count: 10 })

wrapper.setProps({ foo: 'bar' })

#Đạo cụ chế nhạo

Bạn có thể truyền đạo cụ cho thành phần bằng propsDatatùy chọn tích hợp của Vue :

import { mount } from '@vue/test-utils'

mount(Component, {
  propsData: {
    aProp: 'some value'
  }
})

Bạn cũng có thể cập nhật các đạo cụ của một thành phần đã được gắn kết với wrapper.setProps({})phương thức.

Để biết danh sách đầy đủ các tùy chọn, vui lòng xem phần tùy chọn gắn kết của tài liệu.

#Áp dụng Plugin và Mixins toàn cầu

Ví dụ vuex, một số thành phần có thể dựa vào các tính năng được bổ sung bởi plugin hoặc mixin toàn cầu vue-router.

Nếu bạn đang viết bài kiểm tra cho các thành phần trong một ứng dụng cụ thể, bạn có thể thiết lập cùng các plugin và mixin toàn cầu một lần trong mục kiểm tra của mình. Nhưng trong một số trường hợp, ví dụ, thử nghiệm bộ thành phần chung có thể được chia sẻ trên các ứng dụng khác nhau, tốt hơn là kiểm tra các thành phần của bạn trong một thiết lập tách biệt hơn, mà không gây ô nhiễm cho nhà Vuexây dựng toàn cầu . Chúng ta có thể sử dụng createLocalVuephương pháp để đạt được điều đó:

import { createLocalVue } from '@vue/test-utils'

// create an extended `Vue` constructor
const localVue = createLocalVue()

// install plugins as normal
localVue.use(MyPlugin)

// pass the `localVue` to the mount options
mount(Component, {
  localVue
})

Lưu ý một số plugin, như Vue Router, thêm các thuộc tính chỉ đọc vào hàm tạo Vue toàn cầu. Điều này khiến cho không thể cài đặt lại plugin trên hàm localVuetạo hoặc thêm giả cho các thuộc tính chỉ đọc này

#Mocking tiêm

Một chiến lược khác cho các đạo cụ được tiêm chỉ đơn giản là chế nhạo họ. Bạn có thể làm điều đó với mockstùy chọn:

import { mount } from '@vue/test-utils'

const $route = {
  path: '/',
  hash: '',
  params: { id: '123' },
  query: { q: 'hello' }
}

mount(Component, {
  mocks: {
    // adds mocked `$route` object to the Vue instance
    // before mounting component
    $route
  }
})

#Thành phần sơ khai

Bạn có thể ghi đè các thành phần được đăng ký trên toàn cầu hoặc cục bộ bằng cách sử dụng stubstùy chọn:

import { mount } from '@vue/test-utils'

mount(Component, {
  // Will resolve globally-registered-component with
  // empty stub
  stubs: ['globally-registered-component']
})

#Xử lý định tuyến

Do định tuyến theo định nghĩa phải liên quan đến cấu trúc tổng thể của ứng dụng và liên quan đến nhiều thành phần, nên nó được kiểm tra tốt nhất thông qua các thử nghiệm tích hợp hoặc đầu cuối. Đối với các thành phần riêng lẻ dựa trên vue-routercác tính năng, bạn có thể mô phỏng chúng bằng các kỹ thuật được đề cập ở trên.

#Phát hiện kiểu

Thử nghiệm của bạn chỉ có thể phát hiện các kiểu nội tuyến khi chạy trong jsdom.

#Kiểm tra khóa, chuột và các sự kiện DOM khác

#Sự kiện kích hoạt

Việc Wrapperphơi bày một triggerphương pháp. Nó có thể được sử dụng để kích hoạt các sự kiện DOM.

const wrapper = mount(MyButton)

wrapper.trigger('click')

Bạn nên biết rằng findphương thức này cũng trả về Wrapper. Giả sử MyComponentcó chứa một nút, đoạn mã sau nhấp vào nút.

const wrapper = mount(MyComponent)

wrapper.find('button').trigger('click')

#Tùy chọn

Các triggerphương pháp có một tùy chọn optionsđối tượng. Các thuộc tính trong optionsđối tượng được thêm vào Sự kiện.

Lưu ý rằng mục tiêu không thể được thêm vào trong optionsđối tượng.

const wrapper = mount(MyButton)

wrapper.trigger('click', { button: 0 })

#Nhấp chuột Ví dụ

Thành phần được thử nghiệm

<template>
  <div>
    <button class="yes" @click="callYes">Yes</button>
    <button class="no" @click="callNo">No</button>
  </div>
</template>

<script>
export default {
  name: 'YesNoComponent',

  props: {
    callMe: {
      type: Function
    }
  },

  methods: {
    callYes() {
      this.callMe('yes')
    },
    callNo() {
      this.callMe('no')
    }
  }
}
</script>

Kiểm tra

import YesNoComponent from '@/components/YesNoComponent'
import { mount } from '@vue/test-utils'
import sinon from 'sinon'

describe('Click event', () => {
  it('Click on yes button calls our method with argument "yes"', () => {
    const spy = sinon.spy()
    const wrapper = mount(YesNoComponent, {
      propsData: {
        callMe: spy
      }
    })
    wrapper.find('button.yes').trigger('click')

    spy.should.have.been.calledWith('yes')
  })
})

#Ví dụ bàn phím

Thành phần được thử nghiệm

Thành phần này cho phép tăng / giảm số lượng bằng các phím khác nhau.

<template>
  <input type="text" @keydown.prevent="onKeydown" v-model="quantity" />
</template>

<script>
const KEY_DOWN = 40
const KEY_UP = 38
const ESCAPE = 27
const CHAR_A = 65

export default {
  data() {
    return {
      quantity: 0
    }
  },

  methods: {
    increment() {
      this.quantity += 1
    },
    decrement() {
      this.quantity -= 1
    },
    clear() {
      this.quantity = 0
    },
    onKeydown(e) {
      if (e.keyCode === ESCAPE) {
        this.clear()
      }
      if (e.keyCode === KEY_DOWN) {
        this.decrement()
      }
      if (e.keyCode === KEY_UP) {
        this.increment()
      }
      if (e.which === CHAR_A) {
        this.quantity = 13
      }
    }
  },

  watch: {
    quantity: function (newValue) {
      this.$emit('input', newValue)
    }
  }
}
</script>

Kiểm tra

import QuantityComponent from '@/components/QuantityComponent'
import { mount } from '@vue/test-utils'

describe('Key event tests', () => {
  it('Quantity is zero by default', () => {
    const wrapper = mount(QuantityComponent)
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Cursor up sets quantity to 1', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.trigger('keydown.up')
    expect(wrapper.vm.quantity).toBe(1)
  })

  it('Cursor down reduce quantity by 1', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    wrapper.trigger('keydown.down')
    expect(wrapper.vm.quantity).toBe(4)
  })

  it('Escape sets quantity to 0', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    wrapper.trigger('keydown.esc')
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Magic character "a" sets quantity to 13', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.trigger('keydown', {
      which: 65
    })
    expect(wrapper.vm.quantity).toBe(13)
  })
})

Hạn chế

Tên khóa sau dấu chấm keydown.upđược dịch thành a keyCode. Điều này được hỗ trợ cho các tên sau:

tên khóa mã khóa
đi vào 13
thoát 27
chuyển hướng 9
không gian 32
xóa bỏ 46
backspace số 8
chèn 45
lên 38
xuống 40
trái 37
đúng 39
kết thúc 35
nhà 36
trang lên 33
trang dươi 34

#Quan trọng

Vue Test Utils kích hoạt sự kiện đồng bộ. Do đó, Vue.nextTickkhông bắt buộc.

#Chọn người chạy thử

Một người chạy thử là một chương trình chạy thử nghiệm.

Có nhiều người chạy thử nghiệm JavaScript phổ biến và Vue Test Utils hoạt động với tất cả chúng. Đó là thử nghiệm bất khả tri.

Có một vài điều cần xem xét khi chọn một trình chạy thử mặc dù: bộ tính năng, hiệu năng và hỗ trợ cho quá trình biên dịch trước thành phần tệp đơn (SFC). Sau khi so sánh cẩn thận các thư viện hiện có, đây là hai vận động thử nghiệm mà chúng tôi khuyên dùng:

  • Jest là người chạy thử đầy đủ tính năng nhất. Nó yêu cầu cấu hình ít nhất, thiết lập JSDOM theo mặc định, cung cấp các xác nhận tích hợp và có trải nghiệm người dùng dòng lệnh tuyệt vời. Tuy nhiên, bạn sẽ cần một bộ xử lý trước để có thể nhập các thành phần SFC trong các thử nghiệm của mình. Chúng tôi đã tạo vue-jestbộ tiền xử lý có thể xử lý hầu hết các tính năng SFC phổ biến, nhưng hiện tại nó không có tính năng tương đương 100% với vue-loader.

  • mocha-webpack là một trình bao bọc xung quanh webpack + Mocha, nhưng với giao diện và chế độ xem hợp lý hơn. Lợi ích của thiết lập này là chúng tôi có thể nhận được hỗ trợ SFC hoàn chỉnh thông qua webpack + vue-loader, nhưng nó yêu cầu trả trước nhiều cấu hình hơn.

#Môi trường trình duyệt

Vue Test Utils dựa trên môi trường trình duyệt. Về mặt kỹ thuật, bạn có thể chạy nó trong một trình duyệt thực, nhưng điều đó không được khuyến khích do sự phức tạp của việc khởi chạy các trình duyệt thực trên các nền tảng khác nhau. Thay vào đó, chúng tôi khuyên bạn nên chạy thử nghiệm trong Node với môi trường trình duyệt ảo bằng JSDOM .

Người chạy thử Jest tự động thiết lập JSDOM. Đối với những người chạy thử nghiệm khác, bạn có thể thiết lập JSDOM theo cách thủ công cho các bài kiểm tra bằng cách sử dụng jsdom-global trong mục nhập cho các bài kiểm tra của bạn:

npm install --save-dev jsdom jsdom-global

// in test setup / entry
require('jsdom-global')()

#Kiểm tra các thành phần tệp đơn

Các thành phần Vue một tệp (SFC) yêu cầu biên dịch trước trước khi chúng có thể được chạy trong Node hoặc trong trình duyệt. Có hai cách được đề xuất để thực hiện quá trình biên dịch: với bộ tiền xử lý Jest hoặc sử dụng trực tiếp gói webpack.

Bộ vue-jesttiền xử lý hỗ trợ các chức năng SFC cơ bản, nhưng hiện tại không xử lý các khối kiểu hoặc khối tùy chỉnh, chỉ được hỗ trợ trong vue-loader. Nếu bạn dựa vào các tính năng này hoặc các cấu hình dành riêng cho webpack khác, bạn sẽ cần sử dụng vue-loaderthiết lập dựa trên webpack + .

Đọc các hướng dẫn sau đây cho các thiết lập khác nhau:

#Tài nguyên

#Kiểm tra các thành phần tệp đơn với Jest

Một dự án ví dụ cho thiết lập này có sẵn trên GitHub .

Jest là một người chạy thử nghiệm được phát triển bởi Facebook, nhằm mục đích cung cấp một giải pháp thử nghiệm đơn vị bao gồm pin. Bạn có thể tìm hiểu thêm về Jest trên tài liệu chính thức của nó .

#Thiết lập Jest

Chúng tôi sẽ cho rằng bạn đang bắt đầu với một thiết lập đã có webpack, vue-loader và Babel được cấu hình đúng - ví dụ: webpack-simplemẫu được cung cấp bởi vue-cli.

Điều đầu tiên cần làm là cài đặt các ứng dụng thử nghiệm Jest và Vue:

$ npm install --save-dev jest @vue/test-utils

Tiếp theo chúng ta cần xác định một kịch bản thử nghiệm trong package.json.

// package.json
{
  "scripts": {
    "test": "jest"
  }
}

#Xử lý các thành phần tệp đơn trong Jest

Để dạy Jest cách xử lý *.vuetệp, chúng ta sẽ cần cài đặt và định cấu hình vue-jestbộ tiền xử lý:

npm install --save-dev vue-jest

Tiếp theo, tạo một jestkhối trong package.json:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      // tell Jest to handle `*.vue` files
      "vue"
    ],
    "transform": {
      // process `*.vue` files with `vue-jest`
      ".*\\.(vue)$": "vue-jest"
    }
  }
}

Lưu ý: vue-jest hiện tại không hỗ trợ tất cả các tính năng của vue-loader, ví dụ như hỗ trợ khối tùy chỉnh và tải kiểu. Ngoài ra, một số tính năng dành riêng cho webpack như chia tách mã cũng không được hỗ trợ. Để sử dụng các tính năng không được hỗ trợ này, bạn cần sử dụng Mocha thay vì Jest để chạy thử nghiệm và gói webpack để biên dịch các thành phần của bạn. Để bắt đầu, hãy đọc hướng dẫn kiểm tra SFC với gói web Mocha + .

#Xử lý bí danh webpack

Nếu bạn sử dụng một bí danh quyết tâm trong cấu hình webpack, ví dụ như răng cưa @để /src, bạn cần phải thêm một cấu hình phù hợp cho jest là tốt, bằng cách sử dụng moduleNameMappertùy chọn:

{
  // ...
  "jest": {
    // ...
    // support the same @ -> src alias mapping in source code
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    }
  }
}

#Cấu hình Babel cho Jest

Mặc dù các phiên bản mới nhất của Node đã hỗ trợ hầu hết các tính năng ES2015, bạn vẫn có thể muốn sử dụng cú pháp mô-đun ES và các tính năng giai đoạn-x trong các thử nghiệm của mình. Cho rằng chúng ta cần phải cài đặt babel-jest:

npm install --save-dev babel-jest

Tiếp theo, chúng ta cần phải nói đùa để xử lý các file thử nghiệm JavaScript với babel-jestbằng cách thêm một mục dưới jest.transformtrong package.json:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // process js with `babel-jest`
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
    },
    // ...
  }
}

Theo mặc định, babel-jesttự động cấu hình miễn là nó được cài đặt. Tuy nhiên, vì chúng tôi đã thêm một cách rõ ràng một biến đổi cho *.vuecác tệp, bây giờ chúng tôi cũng cần phải cấu hình rõ ràng babel-jest.

Giả sử sử dụng babel-preset-envvới webpack, cấu hình Babel mặc định sẽ vô hiệu hóa quá trình dịch mã mô-đun ES vì webpack đã biết cách xử lý các mô-đun ES. Tuy nhiên, chúng tôi cần phải kích hoạt nó cho các thử nghiệm của mình vì các thử nghiệm Jest chạy trực tiếp trong Node.

Ngoài ra, chúng tôi có thể nói babel-preset-envđể nhắm mục tiêu phiên bản Node mà chúng tôi đang sử dụng. Điều này bỏ qua các tính năng không cần thiết và làm cho các thử nghiệm của chúng tôi khởi động nhanh hơn.

Để chỉ áp dụng các tùy chọn này cho các thử nghiệm, hãy đặt chúng trong một cấu hình riêng biệt bên dưới env.test(điều này sẽ được tự động chọn bởi babel-jest).

Ví dụ .babelrc:

{
  "presets": [
    ["env", { "modules": false }]
  ],
  "env": {
    "test": {
      "presets": [
        ["env", { "targets": { "node": "current" }}]
      ]
    }
  }
}

#Đặt tệp kiểm tra

Theo mặc định, Jest sẽ chọn đệ quy tất cả các tệp có .spec.jshoặc .test.jsmở rộng trong toàn bộ dự án. Nếu điều này không phù hợp với nhu cầu của bạn, có thể thay đổitestRegex phần cấu hình trong package.jsontệp.

Jest khuyên bạn nên tạo một __tests__thư mục ngay bên cạnh mã đang được thử nghiệm, nhưng hãy thoải mái cấu trúc các thử nghiệm của bạn khi bạn thấy phù hợp. Chỉ cần lưu ý rằng Jest sẽ tạo một __snapshots__thư mục bên cạnh các tệp kiểm tra thực hiện kiểm tra ảnh chụp nhanh.

#Bảo hiểm

Jest có thể được sử dụng để tạo báo cáo bảo hiểm ở nhiều định dạng. Sau đây là một ví dụ đơn giản để bắt đầu với:

Mở rộng jestcấu hình của bạn (thường là trong package.jsonhoặc jest.config.js) với tùy chọn, sau đó thêm mảng để xác định các tệp cần thu thập thông tin bảo hiểm.collectCoveragecollectCoverageFrom

{
  "jest": {
    // ...
    "collectCoverage": true,
    "collectCoverageFrom": [
      "**/*.{js,vue}",
      "!**/node_modules/**"
    ]
  }
}

Điều này sẽ cho phép báo cáo bảo hiểm với các phóng viên bảo hiểm mặc định . Bạn có thể tùy chỉnh chúng với coverageReporterstùy chọn:

{
  "jest": {
    // ...
    "coverageReporters": ["html", "text-summary"]
  }
}

Tài liệu khác có thể được tìm thấy trong tài liệu cấu hình Jest , nơi bạn có thể tìm thấy các tùy chọn cho ngưỡng bảo hiểm, thư mục đầu ra mục tiêu, v.v.

#Ví dụ Spec

Nếu bạn quen thuộc với Jasmine, bạn sẽ cảm thấy như ở nhà với API xác nhận của Jest :

import { mount } from '@vue/test-utils'
import Component from './component'

describe('Component', () => {
  test('is a Vue instance', () => {
    const wrapper = mount(Component)
    expect(wrapper.isVueInstance()).toBeTruthy()
  })
})

#Kiểm tra ảnh chụp

Khi bạn gắn kết một thành phần với Vue Test Utils, bạn có quyền truy cập vào phần tử HTML gốc. Điều này có thể được lưu dưới dạng ảnh chụp nhanh để kiểm tra ảnh chụp nhanh của Jest :

test('renders correctly', () => {
  const wrapper = mount(Component)
  expect(wrapper.element).toMatchSnapshot()
})

Chúng tôi có thể cải thiện ảnh chụp nhanh đã lưu bằng trình tuần tự tùy chỉnh:

npm install --save-dev jest-serializer-vue

Sau đó cấu hình nó trong package.json:

{
  // ...
  "jest": {
    // ...
    // serializer for snapshots
    "snapshotSerializers": [
      "jest-serializer-vue"
    ]
  }
}

#Tài nguyên

#Kiểm tra các thành phần tệp đơn với Mocha + webpack

Một dự án ví dụ cho thiết lập này có sẵn trên GitHub .

Một chiến lược khác để thử nghiệm SFC là biên dịch tất cả các thử nghiệm của chúng tôi thông qua webpack và sau đó chạy nó trong một trình chạy thử nghiệm. Ưu điểm của phương pháp này là nó cung cấp cho chúng tôi hỗ trợ đầy đủ cho tất cả các vue-loadertính năng và gói web , vì vậy chúng tôi không phải thỏa hiệp trong mã nguồn của mình.

Về mặt kỹ thuật, bạn có thể sử dụng bất kỳ người chạy thử nào bạn thích và kết nối mọi thứ theo cách thủ công với nhau, nhưng chúng tôi đã tìm thấy để cung cấp trải nghiệm rất hợp lý cho nhiệm vụ cụ thể này.mocha-webpack

#Thiết lậpmocha-webpack

Chúng tôi sẽ cho rằng bạn đang bắt đầu với một thiết lập đã có webpack, vue-loader và Babel được cấu hình đúng - ví dụ: webpack-simplemẫu được cung cấp bởi vue-cli.

Điều đầu tiên cần làm là cài đặt các phụ thuộc kiểm tra:

npm install --save-dev @vue/test-utils mocha mocha-webpack

Tiếp theo chúng ta cần xác định một kịch bản thử nghiệm trong package.json.

// package.json
{
  "scripts": {
    "test": "mocha-webpack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
  }
}

Một vài điều cần lưu ý ở đây:

  • Các --webpack-configcờ xác định tập tin cấu hình webpack để sử dụng cho các bài kiểm tra. Trong hầu hết các trường hợp, điều này sẽ giống hệt với cấu hình bạn sử dụng cho dự án thực tế với một điều chỉnh nhỏ. Chúng ta sẽ nói về điều này sau.

  • Các --requirecờ đảm bảo các tập tin test/setup.jsđược chạy trước bất kỳ thử nghiệm, trong đó chúng ta có thể thiết lập môi trường toàn cầu cho kiểm tra của chúng tôi để được chạy trong.

  • Đối số cuối cùng là một thế giới cho các tệp thử nghiệm được bao gồm trong gói thử nghiệm.

#Cấu hình gói web bổ sung

#Bên ngoài phụ thuộc NPM

Trong các thử nghiệm của chúng tôi, chúng tôi có thể sẽ nhập một số phụ thuộc NPM - một số mô-đun này có thể được viết mà không cần sử dụng trình duyệt và đơn giản là không thể được gói theo gói webpack. Một xem xét khác là phụ thuộc bên ngoài cải thiện đáng kể tốc độ khởi động thử nghiệm. Chúng tôi có thể bên ngoài tất cả các phụ thuộc NPM với webpack-node-externals:

// webpack.config.js
const nodeExternals = require('webpack-node-externals')

module.exports = {
  // ...
  externals: [nodeExternals()]
}

#Bản đồ nguồn

Bản đồ nguồn cần phải được nội tuyến để chọn mocha-webpack. Cấu hình được đề xuất là:

module.exports = {
  // ...
  devtool: 'inline-cheap-module-source-map'
}

Nếu gỡ lỗi qua IDE, bạn cũng nên thêm các mục sau:

module.exports = {
  // ...
  output: {
    // ...
    // use absolute paths in sourcemaps (important for debugging via IDE)
    devtoolModuleFilenameTemplate: '[absolute-resource-path]',
    devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]?[hash]'
  }
}

#Thiết lập môi trường trình duyệt

Vue Test Utils yêu cầu môi trường trình duyệt để chạy. Chúng ta có thể mô phỏng nó trong Node bằng cách sử dụng jsdom-global:

npm install --save-dev jsdom jsdom-global

Sau đó trong test/setup.js:

require('jsdom-global')()

Điều này thêm môi trường trình duyệt vào Node, để Vue Test Utils có thể chạy chính xác.

#Chọn Thư viện Khẳng định

Chai là một thư viện khẳng định phổ biến thường được sử dụng cùng với Mocha. Bạn cũng có thể muốn kiểm tra Sinon để tạo gián điệp và sơ khai.

Ngoài ra, bạn có thể sử dụng expecthiện là một phần của Jest và hiển thị chính xác API tương tựtrong tài liệu của Jest.

Chúng tôi sẽ sử dụng expectở đây và cung cấp nó trên toàn cầu để chúng tôi không phải nhập nó trong mọi thử nghiệm:

npm install --save-dev expect

Sau đó trong test/setup.js:

require('jsdom-global')()

global.expect = require('expect')

#Tối ưu hóa Babel cho các thử nghiệm

Lưu ý rằng chúng tôi đang sử dụng babel-loaderđể xử lý JavaScript. Bạn đã có cấu hình Babel nếu bạn đang sử dụng nó trong ứng dụng của mình thông qua một .babelrctệp. Ở đây babel-loadersẽ tự động sử dụng cùng một tập tin cấu hình.

Một điều cần lưu ý là nếu bạn đang sử dụng Node 6 +, mà đã hỗ trợ hầu hết các tính năng ES2015, bạn có thể cấu hình một riêng biệt Babel tùy chọn env chỉ transpiles tính năng mà chưa được hỗ trợ trong phiên bản Node bạn đang sử dụng (ví dụ stage-2hoặc hỗ trợ cú pháp dòng chảy, v.v.).

#Thêm một bài kiểm tra

Tạo một tệp srccó tên Counter.vue:

<template>
  <div>
    {{ count }}
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}
</script>

Và tạo một tệp thử nghiệm có tên test/Counter.spec.jsvới mã sau đây:

import { shallowMount } from '@vue/test-utils'
import Counter from '../src/Counter.vue'

describe('Counter.vue', () => {
  it('increments count when button is clicked', () => {
    const wrapper = shallowMount(Counter)
    wrapper.find('button').trigger('click')
    expect(wrapper.find('div').text()).toMatch('1')
  })
})

Và bây giờ chúng ta có thể chạy thử nghiệm:

npm run test

Woohoo, chúng tôi đã chạy thử nghiệm của chúng tôi!

#Bảo hiểm

Để bảo hiểm mã cài đặt để mocha-webpack, hãy làm theo các mocha-webpackhướng dẫn mã số bảo hiểm .

#Tài nguyên

#Kiểm tra các thành phần tệp đơn với Karma

Một dự án ví dụ cho thiết lập này có sẵn trên GitHub .

Karma là một người chạy thử nghiệm khởi chạy trình duyệt, chạy thử nghiệm và báo cáo lại cho chúng tôi. Chúng tôi sẽ sử dụng khung Mocha để viết các bài kiểm tra. Chúng tôi sẽ sử dụng thư viện chai để xác nhận thử nghiệm.

#Thiết lập Mocha

Chúng tôi sẽ cho rằng bạn đang bắt đầu với một thiết lập đã có webpack, vue-loader và Babel được cấu hình đúng - ví dụ: webpack-simplemẫu được cung cấp bởi vue-cli.

Điều đầu tiên cần làm là cài đặt các phụ thuộc kiểm tra:

npm install --save-dev @vue/test-utils karma karma-chrome-launcher karma-mocha karma-sourcemap-loader karma-spec-reporter karma-webpack mocha

Tiếp theo chúng ta cần xác định một kịch bản thử nghiệm trong package.json.

// package.json
{
  "scripts": {
    "test": "karma start --single-run"
  }
}
  • Các --single-runlá cờ nói với Karma để chạy các bộ kiểm tra một lần.

Cấu hình #Karma

Tạo một karma.conf.jstệp trong chỉ mục của dự án:

// karma.conf.js

var webpackConfig = require('./webpack.config.js')

module.exports = function (config) {
  config.set({
    frameworks: ['mocha'],

    files: [
      'test/**/*.spec.js'
    ],

    preprocessors: {
      '**/*.spec.js': ['webpack', 'sourcemap']
    },

    webpack: webpackConfig,

    reporters: ['spec'],

    browsers: ['Chrome']
  })
}

Tập tin này được sử dụng để cấu hình Karma.

Chúng tôi cần xử lý trước các tập tin của chúng tôi với webpack. Để làm điều đó, chúng tôi thêm webpack làm bộ xử lý trước và bao gồm cấu hình webpack của chúng tôi. Chúng tôi có thể sử dụng tệp cấu hình webpack trong cơ sở của dự án mà không thay đổi bất cứ điều gì.

Trong cấu hình của chúng tôi, chúng tôi chạy thử nghiệm trong Chrome. Để thêm các trình duyệt bổ sung, hãy xem phần Trình duyệt trong tài liệu Karma .

#Chọn Thư viện Khẳng định

Chai là một thư viện khẳng định phổ biến thường được sử dụng cùng với Mocha. Bạn cũng có thể muốn kiểm tra Sinon để tạo gián điệp và sơ khai.

Chúng tôi có thể cài đặt karma-chaiplugin để sử dụng chaitrong các thử nghiệm của chúng tôi.

npm install --save-dev karma-chai

#Thêm một bài kiểm tra

Tạo một tệp srccó tên Counter.vue:

<template>
  <div>
    {{ count }}
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}
</script>

Và tạo một tệp thử nghiệm có tên test/Counter.spec.jsvới mã sau đây:

import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import Counter from '../src/Counter.vue'

describe('Counter.vue', () => {
  it('increments count when button is clicked', () => {
    const wrapper = shallowMount(Counter)
    wrapper.find('button').trigger('click')
    expect(wrapper.find('div').text()).contains('1')
  })
})

Và bây giờ chúng ta có thể chạy thử nghiệm:

npm run test

Woohoo, chúng tôi đã chạy thử nghiệm của chúng tôi!

#Bảo hiểm

Để thiết lập phạm vi bảo hiểm mã cho Karma, chúng ta có thể sử dụng karma-coverageplugin.

Theo mặc định, karma-coveragesẽ không sử dụng bản đồ nguồn để ánh xạ các báo cáo bảo hiểm. Vì vậy, chúng ta cần sử dụng babel-plugin-istanbulđể đảm bảo vùng phủ sóng được ánh xạ chính xác.

Cài đặt karma-coveragebabel-plugin-istanbulvà cross-env:

npm install --save-dev karma-coverage cross-env

Chúng ta sẽ sử dụng cross-envđể thiết lập một BABEL_ENVbiến môi trường. Bằng cách này, chúng tôi có thể sử dụng babel-plugin-istanbulkhi chúng tôi biên dịch cho các thử nghiệm của mình, chúng tôi không muốn đưa vào babel-plugin-istanbulkhi chúng tôi biên dịch mã sản xuất:

npm install --save-dev babel-plugin-istanbul

Cập nhật .babelrctệp của bạn để sử dụng babel-plugin-istanbulkhi BABEL_ENVđược đặt thành kiểm tra:

{
  "presets": [
    ["env", { "modules": false }],
    "stage-3"
  ],
  "env": {
    "test": {
      "plugins": ["istanbul"]
    }
  }
}

Bây giờ cập nhật các karma.conf.jstập tin để sử dụng bảo hiểm. Thêm coveragevào reportersmảng và thêm một coverageReportertrường:

// karma.conf.js

module.exports = function (config) {
  config.set({
  // ...

    reporters: ['spec', 'coverage'],

    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    }
  })
}

Và cập nhật testtập lệnh để thiết lập BABEL_ENV:

// package.json
{
  "scripts": {
    "test": "cross-env BABEL_ENV=test karma start --single-run"
  }
}

#Tài nguyên

#Kiểm tra hành vi không đồng bộ

Để đơn giản hóa việc kiểm tra, Vue Test Utils áp dụng đồng bộ các bản cập nhật DOM . Tuy nhiên, có một số kỹ thuật bạn cần lưu ý khi kiểm tra một thành phần có hành vi không đồng bộ như gọi lại hoặc hứa hẹn.

Một trong những hành vi không đồng bộ phổ biến nhất là các lệnh gọi API và các hành động Vuex. Các ví dụ sau đây cho thấy cách kiểm tra phương thức thực hiện cuộc gọi API. Ví dụ này sử dụng Jest để chạy thử nghiệm và giả lập thư viện HTTP axios. Thông tin thêm về giả thủ công Jest có thể được tìm thấy ở đây .

Việc thực hiện axiosgiả giống như thế này:

export default {
  get: () => Promise.resolve({ data: 'value' })
}

Thành phần dưới đây thực hiện cuộc gọi API khi nhấp vào nút, sau đó chỉ định phản hồi value.

<template>
  <button @click="fetchResults" />
</template>

<script>
import axios from 'axios'

export default {
  data () {
    return {
      value: null
    }
  },

  methods: {
    async fetchResults () {
      const response = await axios.get('mock/service')
      this.value = response.data
    }
  }
}
</script>

Một bài kiểm tra có thể được viết như thế này:

import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'
jest.mock('axios')

test('Foo', () => {
  it('fetches async when a button is clicked', () => {
    const wrapper = shallowMount(Foo)
    wrapper.find('button').trigger('click')
    expect(wrapper.vm.value).toBe('value')
  })
})

Thử nghiệm này hiện không thành công vì khẳng định được gọi trước khi lời hứa được fetchResultsgiải quyết. Hầu hết các thư viện kiểm tra đơn vị cung cấp một cuộc gọi lại để cho người chạy biết khi kiểm tra hoàn tất. Jest và Mocha đều sử dụng done. Chúng tôi có thể sử dụng donekết hợp với $nextTickhoặc setTimeoutđể đảm bảo mọi lời hứa được giải quyết trước khi xác nhận được đưa ra.

test('Foo', () => {
  it('fetches async when a button is clicked', (done) => {
    const wrapper = shallowMount(Foo)
    wrapper.find('button').trigger('click')
    wrapper.vm.$nextTick(() => {
      expect(wrapper.vm.value).toBe('value')
      done()
    })
  })
})

Lý do $nextTickhoặc setTimeoutcho phép thử nghiệm vượt qua là vì hàng đợi microtask nơi các cuộc gọi lại lời hứa được xử lý chạy trước hàng đợi nhiệm vụ, ở đâu $nextTickvà setTimeoutđược xử lý. Điều này có nghĩa là vào thời điểm $nextTickvà setTimeoutchạy, mọi cuộc gọi lại lời hứa trên hàng đợi microtask sẽ được thực hiện. Xem ở đây để giải thích chi tiết hơn.

Một giải pháp khác là sử dụng một asyncchức năng và gói npm flush-promisesflush-promisestuôn ra tất cả các xử lý lời hứa giải quyết chờ xử lý. Bạn có thể thực awaithiện cuộc gọi flushPromisesđể xóa các lời hứa đang chờ xử lý và cải thiện khả năng đọc của bài kiểm tra của bạn.

Bài kiểm tra cập nhật trông như thế này:

import { shallowMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')

test('Foo', () => {
  it('fetches async when a button is clicked', async () => {
    const wrapper = shallowMount(Foo)
    wrapper.find('button').trigger('click')
    await flushPromises()
    expect(wrapper.vm.value).toBe('value')
  })
})

Kỹ thuật tương tự này có thể được áp dụng cho các hành động của Vuex, mặc định trả lại một lời hứa.

#Sử dụng với TypeScript

Một dự án ví dụ cho thiết lập này có sẵn trên GitHub .

TypeScript là một superset phổ biến của JavaScript có thêm các loại và các lớp trên đầu trang của JS thông thường. Vue Test Utils bao gồm các loại trong gói phân tán, do đó, nó hoạt động tốt với TypeScript.

Trong hướng dẫn này, chúng tôi sẽ hướng dẫn cách thiết lập thiết lập thử nghiệm cho dự án TypeScript bằng cách sử dụng Jest và Vue Test Utils từ thiết lập Vue CLI TypeScript cơ bản.

#Thêm TypeScript

Đầu tiên bạn cần tạo một dự án. Nếu bạn chưa cài đặt Vue CLI, hãy cài đặt nó trên toàn cầu:

$ npm install -g @vue/cli

Và tạo một dự án bằng cách chạy:

$ vue create hello-world

Trong lời nhắc CLI, chọn Manually select features, chọn TypeScript và nhấn enter. Điều này sẽ tạo ra một dự án với TypeScript đã được cấu hình.

CHÚ THÍCH

Nếu bạn muốn có một hướng dẫn chi tiết hơn về việc thiết lập Vue với TypeScript, hãy kiểm tra hướng dẫn khởi động TypeScript Vue .

Bước tiếp theo là thêm Jest vào dự án.

#Thiết lập Jest

Jest là một người chạy thử nghiệm được phát triển bởi Facebook, nhằm mục đích cung cấp một giải pháp thử nghiệm đơn vị bao gồm pin. Bạn có thể tìm hiểu thêm về Jest trên tài liệu chính thức của nó .

Cài đặt các ứng dụng thử nghiệm Jest và Vue:

$ npm install --save-dev jest @vue/test-utils

Tiếp theo xác định một test:unittập lệnh trong package.json.

// package.json
{
  // ..
  "scripts": {
    // ..
    "test:unit": "jest"
  }
  // ..
}

#Xử lý các thành phần tệp đơn trong Jest

Để dạy Jest cách xử lý *.vuetệp, chúng ta cần cài đặt và định cấu hình vue-jestbộ tiền xử lý:

npm install --save-dev vue-jest

Tiếp theo, tạo một jestkhối trong package.json:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "json",
      // tell Jest to handle `*.vue` files
      "vue"
    ],
    "transform": {
      // process `*.vue` files with `vue-jest`
      ".*\\.(vue)$": "vue-jest",
    },
    "testURL": "http://localhost/"
  }
}

#Cấu hình TypeScript cho Jest

Để sử dụng các tệp TypeScript trong các bài kiểm tra, chúng tôi cần thiết lập Jest để biên dịch TypeScript. Cho rằng chúng ta cần phải cài đặt ts-jest:

$ npm install --save-dev ts-jest

Tiếp theo, chúng ta cần phải nói đùa để xử lý các file thử nghiệm nguyên cảo với ts-jestbằng cách thêm một mục dưới jest.transformtrong package.json:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // process `*.ts` files with `ts-jest`
      "^.+\\.tsx?$": "ts-jest"
    },
    // ...
  }
}

#Đặt tệp kiểm tra

Theo mặc định, Jest sẽ chọn đệ quy tất cả các tệp có .spec.jshoặc .test.jsmở rộng trong toàn bộ dự án.

Để chạy các tệp thử nghiệm có .tsphần mở rộng, chúng ta cần thay đổi testRegexphần cấu hình trong package.jsontệp.

Thêm phần sau vào jesttrường trong package.json:

{
  // ...
  "jest": {
    // ...
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
  }
}

Jest khuyên bạn nên tạo một __tests__thư mục ngay bên cạnh mã đang được thử nghiệm, nhưng hãy thoải mái cấu trúc các thử nghiệm của bạn khi bạn thấy phù hợp. Chỉ cần lưu ý rằng Jest sẽ tạo một __snapshots__thư mục bên cạnh các tệp kiểm tra thực hiện kiểm tra ảnh chụp nhanh.

#Viết bài kiểm tra đơn vị

Bây giờ chúng tôi đã thiết lập dự án, đã đến lúc viết bài kiểm tra đơn vị.

Tạo một src/components/__tests__/HelloWorld.spec.tstệp và thêm mã sau đây:

// src/components/__tests__/HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'

describe('HelloWorld.vue', () => {
  test('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

Đó là tất cả những gì chúng ta cần làm để khiến các Trình kiểm tra TypeScript và Vue hoạt động cùng nhau!

#Tài nguyên

#Sử dụng với Bộ định tuyến Vue

#Cài đặt Bộ định tuyến Vue trong các thử nghiệm

Bạn không bao giờ nên cài đặt Bộ định tuyến Vue trên hàm tạo cơ sở Vue trong các thử nghiệm. Cài đặt Bộ định tuyến Vue thêm $routevà $routerdưới dạng các thuộc tính chỉ đọc trên nguyên mẫu Vue.

Để tránh điều này, chúng ta có thể tạo localVue và cài đặt Vue Router trên đó.

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()

shallowMount(Component, {
  localVue,
  router
})

Lưu ý: Cài đặt Bộ định tuyến Vue trên các thuộc tính localVuecũng thêm $routevà $routerdưới dạng chỉ đọc vào a localVue. Điều này có nghĩa là bạn không thể sử dụng mockstùy chọn để ghi đè $routevà $routerkhi gắn một thành phần bằng cách sử dụng localVueBộ định tuyến Vue được cài đặt.

#Kiểm tra các thành phần sử dụng router-linkhoặcrouter-view

Khi bạn cài đặt Vue Router, các thành phần router-linkvà router-viewđược đăng ký. Điều này có nghĩa là chúng ta có thể sử dụng chúng ở bất cứ đâu trong ứng dụng của mình mà không cần phải nhập chúng.

Khi chúng tôi chạy thử nghiệm, chúng tôi cần cung cấp các thành phần Bộ định tuyến Vue này cho thành phần chúng tôi đang lắp. Có hai phương pháp để làm điều này.

#Sử dụng sơ khai

import { shallowMount } from '@vue/test-utils'

shallowMount(Component, {
  stubs: ['router-link', 'router-view']
})

#Cài đặt Bộ định tuyến Vue với localVue

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

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

shallowMount(Component, {
  localVue
})

#Mocking $route$router

Đôi khi bạn muốn kiểm tra xem một thành phần có làm gì đó với các tham số từ $routevà $routercác đối tượng. Để làm điều đó, bạn có thể chuyển các mô phỏng tùy chỉnh cho thể hiện Vue.

import { shallowMount } from '@vue/test-utils'

const $route = {
  path: '/some/path'
}

const wrapper = shallowMount(Component, {
  mocks: {
    $route
  }
})

wrapper.vm.$route.path // /some/path

#Gotchas thông thường

Cài đặt Bộ định tuyến Vue thêm $routevà $routerdưới dạng các thuộc tính chỉ đọc trên nguyên mẫu Vue.

Điều này có nghĩa là bất kỳ bài kiểm tra nào trong tương lai cố gắng chế giễu $routehoặc $routersẽ thất bại.

Để tránh điều này, không bao giờ cài đặt Bộ định tuyến Vue trên toàn cầu khi bạn đang chạy thử nghiệm; sử dụng localVuenhư chi tiết ở trên.

#Sử dụng với Vuex

Trong hướng dẫn này, chúng ta sẽ xem cách kiểm tra Vuex trong các thành phần với Vue Test Utils và cách tiếp cận thử nghiệm cửa hàng Vuex.

#Kiểm tra Vuex trong các thành phần

#Hành động nhạo báng

Hãy xem xét một số mã.

Đây là thành phần chúng tôi muốn kiểm tra. Nó gọi hành động của Vuex.

<template>
  <div class="text-align-center">
    <input type="text" @input="actionInputIfTrue" />
    <button @click="actionClick()">Click</button>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default{
  methods: {
    ...mapActions([
      'actionClick'
    ]),
    actionInputIfTrue: function actionInputIfTrue (event) {
      const inputValue = event.target.value
      if (inputValue === 'input') {
        this.$store.dispatch('actionInput', { inputValue })
      }
    }
  }
}
</script>

Đối với mục đích của thử nghiệm này, chúng tôi không quan tâm các hành động làm gì hoặc cửa hàng trông như thế nào. Chúng ta chỉ cần biết rằng những hành động này đang bị sa thải khi cần, và chúng được sa thải với giá trị mong đợi.

Để kiểm tra điều này, chúng ta cần chuyển một cửa hàng giả cho Vue khi chúng ta nông cạn Thành phần của chúng ta.

Thay vì chuyển cửa hàng đến hàm tạo Vue cơ sở, chúng ta có thể chuyển nó tới a - localVue . LocalVue là một hàm tạo Vue có phạm vi mà chúng ta có thể thay đổi mà không ảnh hưởng đến hàm tạo Vue toàn cầu.

Hãy xem nó trông như thế nào:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Actions'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Actions.vue', () => {
  let actions
  let store

  beforeEach(() => {
    actions = {
      actionClick: jest.fn(),
      actionInput: jest.fn()
    }
    store = new Vuex.Store({
      state: {},
      actions
    })
  })

  it('dispatches "actionInput" when input event value is "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'input'
    input.trigger('input')
    expect(actions.actionInput).toHaveBeenCalled()
  })

  it('does not dispatch "actionInput" when event value is not "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'not input'
    input.trigger('input')
    expect(actions.actionInput).not.toHaveBeenCalled()
  })

  it('calls store action "actionClick" when button is clicked', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    wrapper.find('button').trigger('click')
    expect(actions.actionClick).toHaveBeenCalled()
  })
})

Chuyện gì đang xảy ra ở đây vậy? Đầu tiên chúng tôi nói với Vue sử dụng Vuex với localVue.usephương thức. Đây chỉ là một bọc xung quanh Vue.use.

Sau đó chúng tôi tạo một cửa hàng giả bằng cách gọi new Vuex.Storevới các giá trị giả của chúng tôi. Chúng tôi chỉ thông qua các hành động, vì đó là tất cả những gì chúng tôi quan tâm.

Các hành động là chức năng giả jest . Các hàm giả này cung cấp cho chúng ta các phương thức để xác nhận xem các hành động có được gọi hay không.

Sau đó, chúng tôi có thể khẳng định trong các thử nghiệm của mình rằng cuống hành động được gọi khi dự kiến.

Bây giờ cách chúng tôi xác định cửa hàng có thể trông hơi xa lạ với bạn.

Chúng tôi đang sử dụng beforeEachđể đảm bảo chúng tôi có một cửa hàng sạch trước mỗi bài kiểm tra. beforeEachlà một cái móc mocha được gọi trước mỗi bài kiểm tra. Trong thử nghiệm của chúng tôi, chúng tôi đang gán lại giá trị biến cửa hàng. Nếu chúng ta không làm điều này, các chức năng giả sẽ cần phải được tự động thiết lập lại. Nó cũng cho phép chúng tôi thay đổi trạng thái trong các thử nghiệm của mình mà không ảnh hưởng đến các thử nghiệm sau này.

Điều quan trọng nhất cần lưu ý trong thử nghiệm này là chúng tôi tạo ra một cửa hàng Vuex giả và sau đó chuyển nó đến Vue Test Utils .

Tuyệt vời, vì vậy bây giờ chúng ta có thể chế giễu hành động, chúng ta hãy nhìn vào chế độ chế nhạo.

#Mocking Getters

<template>
  <div>
    <p v-if="inputValue">{{inputValue}}</p>
    <p v-if="clicks">{{clicks}}</p>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default{
  computed: mapGetters([
    'clicks',
    'inputValue'
  ])
}
</script>

Đây là một thành phần khá đơn giản. Nó làm cho kết quả của các getters clicksvà inputValue. Một lần nữa, chúng tôi không thực sự quan tâm đến những gì các getters đó trả về - chỉ là kết quả của chúng được hiển thị chính xác.

Hãy xem thử nghiệm:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Getters from '../../../src/components/Getters'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Getters.vue', () => {
  let getters
  let store

  beforeEach(() => {
    getters = {
      clicks: () => 2,
      inputValue: () => 'input'
    }

    store = new Vuex.Store({
      getters
    })
  })

  it('Renders "state.inputValue" in first p tag', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(getters.inputValue())
  })

  it('Renders "state.clicks" in second p tag', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.findAll('p').at(1)
    expect(p.text()).toBe(getters.clicks().toString())
  })
})

Thử nghiệm này tương tự như thử nghiệm hành động của chúng tôi. Chúng tôi tạo một cửa hàng giả trước mỗi thử nghiệm, vượt qua nó dưới dạng tùy chọn khi chúng tôi gọi shallowMountvà khẳng định rằng giá trị được trả về bởi các getters giả của chúng tôi đang được hiển thị.

Điều này thật tuyệt, nhưng điều gì sẽ xảy ra nếu chúng ta muốn kiểm tra các getters của chúng ta đang trả lại phần chính xác của trạng thái của chúng ta?

#Mocking với Mô-đun

Các mô-đun rất hữu ích để tách cửa hàng của chúng tôi thành các phần có thể quản lý được. Họ cũng xuất khẩu getters. Chúng tôi có thể sử dụng chúng trong các thử nghiệm của chúng tôi.

Hãy nhìn vào thành phần của chúng tôi:

<template>
  <div>
    <button @click="moduleActionClick()">Click</button>
    <p>{{moduleClicks}}</p>
  </div>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'

export default{
  methods: {
    ...mapActions([
      'moduleActionClick'
    ])
  },

  computed: mapGetters([
    'moduleClicks'
  ])
}
</script>

Thành phần đơn giản bao gồm một hành động và một getter.

Và bài kiểm tra:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import MyComponent from '../../../src/components/MyComponent'
import myModule from '../../../src/store/myModule'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('MyComponent.vue', () => {
  let actions
  let state
  let store

  beforeEach(() => {
    state = {
      clicks: 2
    }

    actions = {
      moduleActionClick: jest.fn()
    }

    store = new Vuex.Store({
      modules: {
        myModule: {
          state,
          actions,
          getters: myModule.getters
        }
      }
    })
  })

  it('calls store action "moduleActionClick" when button is clicked', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const button = wrapper.find('button')
    button.trigger('click')
    expect(actions.moduleActionClick).toHaveBeenCalled()
  })

  it('renders "state.inputValue" in first p tag', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(state.clicks.toString())
  })
})

# Đangkiểm tra Cửa hàng Vuex

Có hai cách tiếp cận để thử nghiệm một cửa hàng Vuex. Cách tiếp cận đầu tiên là kiểm tra đơn vị các getters, đột biến và hành động một cách riêng biệt. Cách tiếp cận thứ hai là tạo ra một cửa hàng và thử nghiệm chống lại điều đó. Chúng ta sẽ xem xét cả hai cách tiếp cận.

Để xem cách kiểm tra cửa hàng Vuex, chúng tôi sẽ tạo một cửa hàng truy cập đơn giản. Các cửa hàng sẽ có một incrementđột biến và một evenOrOddgetter.

// mutations.js
export default {
  increment (state) {
    state.count++
  }
}
// getters.js
export default {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}

#Kiểm tra getters, đột biến và hành động riêng biệt

Getters, đột biến và hành động đều là các hàm JavaScript, vì vậy chúng tôi có thể kiểm tra chúng mà không cần sử dụng Vue Test Utils và Vuex.

Lợi ích của việc kiểm tra getters, đột biến và hành động một cách riêng biệt là các bài kiểm tra đơn vị của bạn được chi tiết. Khi họ thất bại, bạn biết chính xác những gì sai với mã của bạn. Nhược điểm là bạn sẽ cần phải chế nhạo các hàm Vuex, như commitvà dispatch. Điều này có thể dẫn đến tình huống đơn vị kiểm tra của bạn vượt qua, nhưng mã sản xuất của bạn không thành công vì giả định của bạn không chính xác.

Chúng tôi sẽ tạo hai tệp thử nghiệm mutations.spec.jsvà getters.spec.js:

Đầu tiên, hãy kiểm tra các incrementđột biến:

// mutations.spec.js

import mutations from './mutations'

test('increment increments state.count by 1', () => {
  const state = {
    count: 0
  }
  mutations.increment(state)
  expect(state.count).toBe(1)
})

Bây giờ hãy kiểm tra evenOrOddgetter. Chúng ta có thể kiểm tra nó bằng cách tạo một bản giả state, gọi getter bằng statevà kiểm tra nó trả về giá trị chính xác.

// getters.spec.js

import getters from './getters'

test('evenOrOdd returns even if state.count is even', () => {
  const state = {
    count: 2
  }
  expect(getters.evenOrOdd(state)).toBe('even')
})

test('evenOrOdd returns odd if state.count is odd', () => {
  const state = {
    count: 1
  }
  expect(getters.evenOrOdd(state)).toBe('odd')
})

#Kiểm tra một cửa hàng đang chạy

Một cách tiếp cận khác để kiểm tra cửa hàng Vuex là tạo một cửa hàng đang chạy bằng cách sử dụng cấu hình cửa hàng.

Lợi ích của việc thử nghiệm tạo một phiên bản cửa hàng đang chạy là chúng ta không phải chế giễu bất kỳ chức năng Vuex nào.

Nhược điểm là khi một bài kiểm tra bị phá vỡ, có thể khó tìm ra vấn đề ở đâu.

Hãy viết một bài kiểm tra. Khi chúng tôi tạo một cửa hàng, chúng tôi sẽ sử dụng localVueđể tránh gây ô nhiễm cho nhà xây dựng cơ sở Vue. Thử nghiệm tạo ra một cửa hàng bằng cách store-config.jsxuất:

// store-config.spec.js

import mutations from './mutations'
import getters from './getters'

export default {
  state: {
    count: 0
  },
  mutations,
  getters
}
import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import storeConfig from './store-config'
import { cloneDeep } from 'lodash'

test('increments count value when increment is commited', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.state.count).toBe(0)
  store.commit('increment')
  expect(store.state.count).toBe(1)
})

test('updates evenOrOdd getter when increment is commited', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.getters.evenOrOdd).toBe('even')
  store.commit('increment')
  expect(store.getters.evenOrOdd).toBe('odd')
})

Lưu ý rằng chúng tôi sử dụng cloneDeepđể sao chép cấu hình cửa hàng trước khi tạo một cửa hàng với nó. Điều này là do Vuex làm thay đổi đối tượng tùy chọn được sử dụng để tạo cửa hàng. Để đảm bảo chúng tôi có một cửa hàng sạch trong mỗi thử nghiệm, chúng tôi cần sao chép storeConfigđối tượng.

#Tài nguyên

Getting Started

#Setup

To get a quick taste of using Vue Test Utils, clone our demo repository with basic setup and install the dependencies:

git clone https://github.com/vuejs/vue-test-utils-getting-started
cd vue-test-utils-getting-started
npm install

You will see that the project includes a simple component, counter.js:

// counter.js

export default {
  template: `
    <div>
      <span class="count">{{ count }}</span>
      <button @click="increment">Increment</button>
    </div>
  `,

  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}

#Mounting Components

Vue Test Utils tests Vue components by mounting them in isolation, mocking the necessary inputs (props, injections and user events) and asserting the outputs (render result, emitted custom events).

Mounted components are returned inside a Wrapper, which exposes many convenience methods for manipulating, traversing and querying the underlying Vue component instance.

You can create wrappers using the mount method. Let's create a file called test.js:

// test.js

// Import the `mount()` method from the test utils
// and the component you want to test
import { mount } from '@vue/test-utils'
import Counter from './counter'

// Now mount the component and you have the wrapper
const wrapper = mount(Counter)

// You can access the actual Vue instance via `wrapper.vm`
const vm = wrapper.vm

// To inspect the wrapper deeper just log it to the console
// and your adventure with the Vue Test Utils begins
console.log(wrapper)

#Test rendered HTML output of the component

Now that we have the wrapper, the first thing we can do is to verify that the rendered HTML output of the component matches what is expected.

import { mount } from '@vue/test-utils'
import Counter from './counter'

describe('Counter', () => {
  // Now mount the component and you have the wrapper
  const wrapper = mount(Counter)

  it('renders the correct markup', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // it's also easy to check for the existence of elements
  it('has a button', () => {
    expect(wrapper.contains('button')).toBe(true)
  })
})

Now run the tests with npm test. You should see the tests passing.

#Simulating User Interaction

Our counter should increment the count when the user clicks the button. To simulate the behavior, we need to first locate the button with wrapper.find(), which returns a wrapper for the button element. We can then simulate the click by calling .trigger() on the button wrapper:

it('button click should increment the count', () => {
  expect(wrapper.vm.count).toBe(0)
  const button = wrapper.find('button')
  button.trigger('click')
  expect(wrapper.vm.count).toBe(1)
})

#What about nextTick?

Vue batches pending DOM updates and applies them asynchronously to prevent unnecessary re-renders caused by multiple data mutations. This is why in practice we often have to use Vue.nextTick to wait until Vue has performed the actual DOM update after we trigger some state change.

To simplify usage, Vue Test Utils applies all updates synchronously so you don't need to use Vue.nextTick to wait for DOM updates in your tests.

Note: nextTick is still necessary when you need to explictly advance the event loop, for operations such as asynchronous callbacks or promise resolution.

If you do still need to use nextTick in your test files, be aware that any errors thrown inside it may not be caught by your test runner as it uses promises internally. There are two approaches to fixing this: either you can set the done callback as Vue's global error handler at the start of the test, or you can call nextTick without an argument and return it as a promise:

// this will not be caught
it('will time out', (done) => {
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

// the two following tests will work as expected
it('will catch the error using done', (done) => {
  Vue.config.errorHandler = done
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

it('will catch the error using a promise', () => {
  return Vue.nextTick()
    .then(function () {
      expect(true).toBe(false)
    })
})

#What's Next

#Common Tips

#Knowing What to Test

For UI components, we don't recommend aiming for complete line-based coverage, because it leads to too much focus on the internal implementation details of the components and could result in brittle tests.

Instead, we recommend writing tests that assert your component's public interface, and treat its internals as a black box. A single test case would assert that some input (user interaction or change of props) provided to the component results in the expected output (render result or emitted custom events).

For example, for the Counter component which increments a display counter by 1 each time a button is clicked, its test case would simulate the click and assert that the rendered output has increased by 1. The test doesn't care about how the Counter increments the value, it only cares about the input and the output.

The benefit of this approach is that as long as your component's public interface remains the same, your tests will pass no matter how the component's internal implementation changes over time.

This topic is discussed with more details in a great presentation by Matt O'Connell.

#Shallow Rendering

In unit tests, we typically want to focus on the component being tested as an isolated unit and avoid indirectly asserting the behavior of its child components.

In addition, for components that contain many child components, the entire rendered tree can get really big. Repeatedly rendering all child components could slow down our tests.

Vue Test Utils allows you to mount a component without rendering its child components (by stubbing them) with the shallowMount method:

import { shallowMount } from '@vue/test-utils'

const wrapper = shallowMount(Component)
wrapper.vm // the mounted Vue instance

#Asserting Emitted Events

Each mounted wrapper automatically records all events emitted by the underlying Vue instance. You can retrieve the recorded events using the wrapper.emitted() method:

wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)

/*
`wrapper.emitted()` returns the following object:
{
  foo: [[], [123]]
}
*/

You can then make assertions based on these data:

// assert event has been emitted
expect(wrapper.emitted().foo).toBeTruthy()

// assert event count
expect(wrapper.emitted().foo.length).toBe(2)

// assert event payload
expect(wrapper.emitted().foo[1]).toEqual([123])

You can also get an Array of the events in their emit order by calling wrapper.emittedByOrder().

#Emitting Event from Child Component

You can emit a custom event from a child component by accessing the instance.

Component under test

<template>
  <div>
    <child-component @custom="onCustom"/>
    <p v-if="emitted">Emitted!</p>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'

export default {
    name: 'ParentComponent',
    components: { ChildComponent },
    data() {
        return {
            emitted: false
        }
    },
    methods: {
        onCustom () {
            this.emitted = true
        }
    }
}
</script>

Test

import { shallowMount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent'
import ChildComponent from '@/components/ChildComponent'

describe('ParentComponent', () => {
  it("displays 'Emitted!' when custom event is emitted", () => {
    const wrapper = shallowMount(ParentComponent)
    wrapper.find(ChildComponent).vm.$emit('custom')
    expect(wrapper.html()).toContain('Emitted!')
  })
})

#Manipulating Component State

You can directly manipulate the state of the component using the setData or setProps method on the wrapper:

wrapper.setData({ count: 10 })

wrapper.setProps({ foo: 'bar' })

#Mocking Props

You can pass props to the component using Vue's built-in propsData option:

import { mount } from '@vue/test-utils'

mount(Component, {
  propsData: {
    aProp: 'some value'
  }
})

You can also update the props of an already-mounted component with the wrapper.setProps({})method.

For a full list of options, please see the mount options section of the docs.

#Applying Global Plugins and Mixins

Some of the components may rely on features injected by a global plugin or mixin, for example vuexand vue-router.

If you are writing tests for components in a specific app, you can setup the same global plugins and mixins once in the entry of your tests. But in some cases, for example testing a generic component suite that may get shared across different apps, it's better to test your components in a more isolated setup, without polluting the global Vue constructor. We can use the createLocalVue method to achieve that:

import { createLocalVue } from '@vue/test-utils'

// create an extended `Vue` constructor
const localVue = createLocalVue()

// install plugins as normal
localVue.use(MyPlugin)

// pass the `localVue` to the mount options
mount(Component, {
  localVue
})

Note some plugins, like Vue Router, add read-only properties to the global Vue constructor. This makes it impossible to reinstall the plugin on a localVue constructor, or add mocks for these read-only properties

#Mocking Injections

Another strategy for injected props is simply mocking them. You can do that with the mocks option:

import { mount } from '@vue/test-utils'

const $route = {
  path: '/',
  hash: '',
  params: { id: '123' },
  query: { q: 'hello' }
}

mount(Component, {
  mocks: {
    // adds mocked `$route` object to the Vue instance
    // before mounting component
    $route
  }
})

#Stubbing components

You can override components that are registered globally or locally by using the stubs option:

import { mount } from '@vue/test-utils'

mount(Component, {
  // Will resolve globally-registered-component with
  // empty stub
  stubs: ['globally-registered-component']
})

#Dealing with Routing

Since routing by definition has to do with the overall structure of the application and involves multiple components, it is best tested via integration or end-to-end tests. For individual components that rely on vue-router features, you can mock them using the techniques mentioned above.

#Detecting styles

Your test can only detect inline styles when running in jsdom.

#Testing Key, Mouse and other DOM events

#Trigger events

The Wrapper expose a trigger method. It can be used to trigger DOM events.

const wrapper = mount(MyButton)

wrapper.trigger('click')

You should be aware that the find method returns a Wrapper as well. Assuming MyComponentcontains a button, the following code clicks the button.

const wrapper = mount(MyComponent)

wrapper.find('button').trigger('click')

#Options

The trigger method takes an optional options object. The properties in the options object are added to the Event.

Note that target cannot be added in the options object.

const wrapper = mount(MyButton)

wrapper.trigger('click', { button: 0 })

#Mouse Click Example

Component under test

<template>
  <div>
    <button class="yes" @click="callYes">Yes</button>
    <button class="no" @click="callNo">No</button>
  </div>
</template>

<script>
export default {
  name: 'YesNoComponent',

  props: {
    callMe: {
      type: Function
    }
  },

  methods: {
    callYes() {
      this.callMe('yes')
    },
    callNo() {
      this.callMe('no')
    }
  }
}
</script>

Test

import YesNoComponent from '@/components/YesNoComponent'
import { mount } from '@vue/test-utils'
import sinon from 'sinon'

describe('Click event', () => {
  it('Click on yes button calls our method with argument "yes"', () => {
    const spy = sinon.spy()
    const wrapper = mount(YesNoComponent, {
      propsData: {
        callMe: spy
      }
    })
    wrapper.find('button.yes').trigger('click')

    spy.should.have.been.calledWith('yes')
  })
})

#Keyboard Example

Component under test

This component allows to increment/decrement the quantity using various keys.

<template>
  <input type="text" @keydown.prevent="onKeydown" v-model="quantity" />
</template>

<script>
const KEY_DOWN = 40
const KEY_UP = 38
const ESCAPE = 27
const CHAR_A = 65

export default {
  data() {
    return {
      quantity: 0
    }
  },

  methods: {
    increment() {
      this.quantity += 1
    },
    decrement() {
      this.quantity -= 1
    },
    clear() {
      this.quantity = 0
    },
    onKeydown(e) {
      if (e.keyCode === ESCAPE) {
        this.clear()
      }
      if (e.keyCode === KEY_DOWN) {
        this.decrement()
      }
      if (e.keyCode === KEY_UP) {
        this.increment()
      }
      if (e.which === CHAR_A) {
        this.quantity = 13
      }
    }
  },

  watch: {
    quantity: function (newValue) {
      this.$emit('input', newValue)
    }
  }
}
</script>

Test

import QuantityComponent from '@/components/QuantityComponent'
import { mount } from '@vue/test-utils'

describe('Key event tests', () => {
  it('Quantity is zero by default', () => {
    const wrapper = mount(QuantityComponent)
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Cursor up sets quantity to 1', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.trigger('keydown.up')
    expect(wrapper.vm.quantity).toBe(1)
  })

  it('Cursor down reduce quantity by 1', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    wrapper.trigger('keydown.down')
    expect(wrapper.vm.quantity).toBe(4)
  })

  it('Escape sets quantity to 0', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    wrapper.trigger('keydown.esc')
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Magic character "a" sets quantity to 13', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.trigger('keydown', {
      which: 65
    })
    expect(wrapper.vm.quantity).toBe(13)
  })
})

Limitations

A key name after the dot keydown.up is translated to a keyCode. This is supported for the following names:

key name key code
enter 13
esc 27
tab 9
space 32
delete 46
backspace 8
insert 45
up 38
down 40
left 37
right 39
end 35
home 36
pageup 33
pagedown 34

#Important

Vue Test Utils triggers event synchronously. Consequently, Vue.nextTick is not required.

#Choosing a test runner

A test runner is a program that runs tests.

There are many popular JavaScript test runners, and Vue Test Utils works with all of them. It's test runner agnostic.

There are a few things to consider when choosing a test runner though: feature set, performance, and support for single-file component (SFC) pre-compilation. After carefully comparing existing libraries, here are two test runners that we recommend:

  • Jest is the most fully featured test runner. It requires the least configuration, sets up JSDOM by default, provides built-in assertions, and has a great command line user experience. However, you will need a preprocessor to be able to import SFC components in your tests. We have created the vue-jest preprocessor which can handle most common SFC features, but it currently does not have 100% feature parity with vue-loader.

  • mocha-webpack is a wrapper around webpack + Mocha, but with a more streamlined interface and watch mode. The benefits of this setup is that we can get complete SFC support via webpack + vue-loader, but it requires more configuration upfront.

#Browser Environment

Vue Test Utils relies on a browser environment. Technically you can run it in a real browser, but it's not recommended due to the complexity of launching real browsers on different platforms. Instead, we recommend running the tests in Node with a virtual browser environment using JSDOM.

The Jest test runner sets up JSDOM automatically. For other test runners, you can manually set up JSDOM for the tests using jsdom-global in the entry for your tests:

npm install --save-dev jsdom jsdom-global

// in test setup / entry
require('jsdom-global')()

#Testing Single-File Components

Single-file Vue components (SFCs) require pre-compilation before they can be run in Node or in the browser. There are two recommended ways to perform the compilation: with a Jest preprocessor, or directly use webpack.

The vue-jest preprocessor supports basic SFC functionalities, but currently does not handle style blocks or custom blocks, which are only supported in vue-loader. If you rely on these features or other webpack-specific configurations, you will need to use a webpack + vue-loader based setup.

Read the following guides for different setups:

#Resources

#Testing Single-File Components with Jest

An example project for this setup is available on GitHub.

Jest is a test runner developed by Facebook, aiming to deliver a battery-included unit testing solution. You can learn more about Jest on its official documentation.

#Setting up Jest

We will assume you are starting with a setup that already has webpack, vue-loader and Babel properly configured - e.g. the webpack-simple template scaffolded by vue-cli.

The first thing to do is install Jest and Vue Test Utils:

$ npm install --save-dev jest @vue/test-utils

Next we need to define a test script in our package.json.

// package.json
{
  "scripts": {
    "test": "jest"
  }
}

#Processing Single-File Components in Jest

To teach Jest how to process *.vue files, we will need to install and configure the vue-jestpreprocessor:

npm install --save-dev vue-jest

Next, create a jest block in package.json:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      // tell Jest to handle `*.vue` files
      "vue"
    ],
    "transform": {
      // process `*.vue` files with `vue-jest`
      ".*\\.(vue)$": "vue-jest"
    }
  }
}

Note: vue-jest currently does not support all the features of vue-loader, for example custom block support and style loading. In addition, some webpack-specific features such as code-splitting are not supported either. To use these unsupported features, you need to use Mocha instead of Jest to run your tests, and webpack to compile your components. To get started, read the guide on testing SFCs with Mocha + webpack.

#Handling webpack Aliases

If you use a resolve alias in the webpack config, e.g. aliasing @ to /src, you need to add a matching config for Jest as well, using the moduleNameMapper option:

{
  // ...
  "jest": {
    // ...
    // support the same @ -> src alias mapping in source code
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    }
  }
}

#Configuring Babel for Jest

Although latest versions of Node already supports most ES2015 features, you may still want to use ES modules syntax and stage-x features in your tests. For that we need to install babel-jest:

npm install --save-dev babel-jest

Next, we need to tell Jest to process JavaScript test files with babel-jest by adding an entry under jest.transform in package.json:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // process js with `babel-jest`
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
    },
    // ...
  }
}

By default, babel-jest automatically configures itself as long as it's installed. However, because we have explicitly added a transform for *.vue files, we now need to explicitly configure babel-jest as well.

Assuming using babel-preset-env with webpack, the default Babel config disables ES modules transpilation because webpack already knows how to handle ES modules. However, we do need to enable it for our tests because Jest tests run directly in Node.

Also, we can tell babel-preset-env to target the Node version we are using. This skips transpiling unnecessary features and makes our tests boot faster.

To apply these options only for tests, put them in a separate config under env.test (this will be automatically picked up by babel-jest).

Example .babelrc:

{
  "presets": [
    ["env", { "modules": false }]
  ],
  "env": {
    "test": {
      "presets": [
        ["env", { "targets": { "node": "current" }}]
      ]
    }
  }
}

#Placing Test Files

By default, Jest will recursively pick up all files that have a .spec.js or .test.js extension in the entire project. If this does not fit your needs, it's possible to change the testRegex in the config section in the package.json file.

Jest recommends creating a __tests__ directory right next to the code being tested, but feel free to structure your tests as you see fit. Just beware that Jest would create a __snapshots__ directory next to test files that performs snapshot testing.

#Coverage

Jest can be used to generate coverage reports in multiple formats. The following is a simple example to get started with:

Extend your jest config (usually in package.json or jest.config.js) with the collectCoverageoption, and then add the collectCoverageFrom array to define the files for which coverage information should be collected.

{
  "jest": {
    // ...
    "collectCoverage": true,
    "collectCoverageFrom": [
      "**/*.{js,vue}",
      "!**/node_modules/**"
    ]
  }
}

This will enable coverage reports with the default coverage reporters. You can customise these with the coverageReporters option:

{
  "jest": {
    // ...
    "coverageReporters": ["html", "text-summary"]
  }
}

Further documentation can be found in the Jest configuration documentation, where you can find options for coverage thresholds, target output directories, etc.

#Example Spec

If you are familiar with Jasmine, you should feel right at home with Jest's assertion API:

import { mount } from '@vue/test-utils'
import Component from './component'

describe('Component', () => {
  test('is a Vue instance', () => {
    const wrapper = mount(Component)
    expect(wrapper.isVueInstance()).toBeTruthy()
  })
})

#Snapshot Testing

When you mount a component with Vue Test Utils, you get access to the root HTML element. This can be saved as a snapshot for Jest snapshot testing:

test('renders correctly', () => {
  const wrapper = mount(Component)
  expect(wrapper.element).toMatchSnapshot()
})

We can improve the saved snapshot with a custom serializer:

npm install --save-dev jest-serializer-vue

Then configure it in package.json:

{
  // ...
  "jest": {
    // ...
    // serializer for snapshots
    "snapshotSerializers": [
      "jest-serializer-vue"
    ]
  }
}

#Resources

#Testing Single-File Components with Mocha + webpack

An example project for this setup is available on GitHub.

Another strategy for testing SFCs is compiling all our tests via webpack and then run it in a test runner. The advantage of this approach is that it gives us full support for all webpack and vue-loader features, so we don't have to make compromises in our source code.

You can technically use any test runner you like and manually wire things together, but we've found mocha-webpack to provide a very streamlined experience for this particular task.

#Setting Up mocha-webpack

We will assume you are starting with a setup that already has webpack, vue-loader and Babel properly configured - e.g. the webpack-simple template scaffolded by vue-cli.

The first thing to do is installing test dependencies:

npm install --save-dev @vue/test-utils mocha mocha-webpack

Next we need to define a test script in our package.json.

// package.json
{
  "scripts": {
    "test": "mocha-webpack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
  }
}

A few things to note here:

  • The --webpack-config flag specifies the webpack config file to use for the tests. In most cases, this would be identical to the config you use for the actual project with one small tweak. We will talk about this later.

  • The --require flag ensures the file test/setup.js is run before any tests, in which we can setup the global environment for our tests to be run in.

  • The final argument is a glob for the test files to be included in the test bundle.

#Extra webpack Configuration

#Externalizing NPM Dependencies

In our tests we will likely import a number of NPM dependencies - some of these modules may be written without browser usage in mind and simply cannot be bundled properly by webpack. Another consideration is externalizing dependencies greatly improves test boot up speed. We can externalize all NPM dependencies with webpack-node-externals:

// webpack.config.js
const nodeExternals = require('webpack-node-externals')

module.exports = {
  // ...
  externals: [nodeExternals()]
}

#Source Maps

Source maps need to be inlined to be picked up by mocha-webpack. The recommended config is:

module.exports = {
  // ...
  devtool: 'inline-cheap-module-source-map'
}

If debugging via IDE, it's also recommended to add the following:

module.exports = {
  // ...
  output: {
    // ...
    // use absolute paths in sourcemaps (important for debugging via IDE)
    devtoolModuleFilenameTemplate: '[absolute-resource-path]',
    devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]?[hash]'
  }
}

#Setting Up Browser Environment

Vue Test Utils requires a browser environment to run. We can simulate it in Node using jsdom-global:

npm install --save-dev jsdom jsdom-global

Then in test/setup.js:

require('jsdom-global')()

This adds a browser environment to Node, so that Vue Test Utils can run correctly.

#Picking an Assertion Library

Chai is a popular assertion library that is commonly used alongside Mocha. You may also want to check out Sinon for creating spies and stubs.

Alternatively you can use expect which is now part of Jest, and exposes the exact same API in Jest docs.

We will be using expect here and make it globally available so that we don't have to import it in every test:

npm install --save-dev expect

Then in test/setup.js:

require('jsdom-global')()

global.expect = require('expect')

#Optimizing Babel for Tests

Notice that we are using babel-loader to handle JavaScript. You should already have Babel configured if you are using it in your app via a .babelrc file. Here babel-loader will automatically use the same config file.

One thing to note is that if you are using Node 6+, which already supports the majority of ES2015 features, you can configure a separate Babel env option that only transpiles features that are not already supported in the Node version you are using (e.g. stage-2 or flow syntax support, etc.).

#Adding a test

Create a file in src named Counter.vue:

<template>
  <div>
    {{ count }}
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}
</script>

And create a test file named test/Counter.spec.js with the following code:

import { shallowMount } from '@vue/test-utils'
import Counter from '../src/Counter.vue'

describe('Counter.vue', () => {
  it('increments count when button is clicked', () => {
    const wrapper = shallowMount(Counter)
    wrapper.find('button').trigger('click')
    expect(wrapper.find('div').text()).toMatch('1')
  })
})

And now we can run the test:

npm run test

Woohoo, we got our tests running!

#Coverage

To setup code coverage to mocha-webpack, follow the mocha-webpack code coverage guide.

#Resources

#Testing Single-File Components with Karma

An example project for this setup is available on GitHub.

Karma is a test runner that launches browsers, runs tests, and reports them back to us. We're going to use the Mocha framework to write the tests. We'll use the chai library for test assertions.

#Setting up Mocha

We will assume you are starting with a setup that already has webpack, vue-loader and Babel properly configured - e.g. the webpack-simple template scaffolded by vue-cli.

The first thing to do is install the test dependencies:

npm install --save-dev @vue/test-utils karma karma-chrome-launcher karma-mocha karma-sourcemap-loader karma-spec-reporter karma-webpack mocha

Next we need to define a test script in our package.json.

// package.json
{
  "scripts": {
    "test": "karma start --single-run"
  }
}
  • The --single-run flag tells Karma to run the test suite once.

#Karma Configuration

Create a karma.conf.js file in the index of the project:

// karma.conf.js

var webpackConfig = require('./webpack.config.js')

module.exports = function (config) {
  config.set({
    frameworks: ['mocha'],

    files: [
      'test/**/*.spec.js'
    ],

    preprocessors: {
      '**/*.spec.js': ['webpack', 'sourcemap']
    },

    webpack: webpackConfig,

    reporters: ['spec'],

    browsers: ['Chrome']
  })
}

This file is used to configure Karma.

We need to preprocess our files with webpack. to do that, we add webpack as a preprocessor, and include our webpack config. We can use the webpack config file in the base of the project without changing anything.

In our configuration, we run the tests in Chrome. To add extra browsers, see the Browsers section in the Karma docs.

#Picking an Assertion Library

Chai is a popular assertion library that is commonly used alongside Mocha. You may also want to check out Sinon for creating spies and stubs.

We can install the karma-chai plugin to use chai in our tests.

npm install --save-dev karma-chai

#Adding a test

Create a file in src named Counter.vue:

<template>
  <div>
    {{ count }}
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}
</script>

And create a test file named test/Counter.spec.js with the following code:

import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import Counter from '../src/Counter.vue'

describe('Counter.vue', () => {
  it('increments count when button is clicked', () => {
    const wrapper = shallowMount(Counter)
    wrapper.find('button').trigger('click')
    expect(wrapper.find('div').text()).contains('1')
  })
})

And now we can run the tests:

npm run test

Woohoo, we got our tests running!

#Coverage

To setup code coverage to Karma, we can use the karma-coverage plugin.

By default, karma-coverage won't use source maps to map the coverage reports. So we need to use babel-plugin-istanbul to make sure the coverage is mapped correctly.

Install karma-coveragebabel-plugin-istanbul, and cross-env:

npm install --save-dev karma-coverage cross-env

We're going to use cross-env to set a BABEL_ENV environment variable. This way we can use babel-plugin-istanbul when we're compiling for our tests—we don't want to include babel-plugin-istanbulwhen we compile our production code:

npm install --save-dev babel-plugin-istanbul

Update your .babelrc file to use babel-plugin-istanbul when BABEL_ENV is set to test:

{
  "presets": [
    ["env", { "modules": false }],
    "stage-3"
  ],
  "env": {
    "test": {
      "plugins": ["istanbul"]
    }
  }
}

Now update the karma.conf.js file to use coverage. Add coverage to the reporters array, and add a coverageReporter field:

// karma.conf.js

module.exports = function (config) {
  config.set({
  // ...

    reporters: ['spec', 'coverage'],

    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    }
  })
}

And update the test script to set the BABEL_ENV:

// package.json
{
  "scripts": {
    "test": "cross-env BABEL_ENV=test karma start --single-run"
  }
}

#Resources

#Testing Asynchronous Behavior

To simplify testing, Vue Test Utils applies DOM updates synchronously. However, there are some techniques you need to be aware of when testing a component with asynchronous behavior such as callbacks or promises.

One of the most common asynchronous behaviors is API calls and Vuex actions. The following examples shows how to test a method that makes an API call. This example uses Jest to run the test and to mock the HTTP library axios. More about Jest manual mocks can be found here.

The implementation of the axios mock looks like this:

export default {
  get: () => Promise.resolve({ data: 'value' })
}

The below component makes an API call when a button is clicked, then assigns the response to value.

<template>
  <button @click="fetchResults" />
</template>

<script>
import axios from 'axios'

export default {
  data () {
    return {
      value: null
    }
  },

  methods: {
    async fetchResults () {
      const response = await axios.get('mock/service')
      this.value = response.data
    }
  }
}
</script>

A test can be written like this:

import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'
jest.mock('axios')

test('Foo', () => {
  it('fetches async when a button is clicked', () => {
    const wrapper = shallowMount(Foo)
    wrapper.find('button').trigger('click')
    expect(wrapper.vm.value).toBe('value')
  })
})

This test currently fails because the assertion is called before the promise in fetchResults resolves. Most unit test libraries provide a callback to let the runner know when the test is complete. Jest and Mocha both use done. We can use done in combination with $nextTick or setTimeout to ensure any promises resolve before the assertion is made.

test('Foo', () => {
  it('fetches async when a button is clicked', (done) => {
    const wrapper = shallowMount(Foo)
    wrapper.find('button').trigger('click')
    wrapper.vm.$nextTick(() => {
      expect(wrapper.vm.value).toBe('value')
      done()
    })
  })
})

The reason $nextTick or setTimeout allow the test to pass is because the microtask queue where promise callbacks are processed run before the task queue, where $nextTick and setTimeout are processed. This means by the time the $nextTick and setTimeout run, any promise callbacks on the microtask queue will have been executed. See here for a more detailed explanation.

Another solution is to use an async function and the npm package flush-promisesflush-promisesflushes all pending resolved promise handlers. You can await the call of flushPromises to flush pending promises and improve the readability of your test.

The updated test looks like this:

import { shallowMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')

test('Foo', () => {
  it('fetches async when a button is clicked', async () => {
    const wrapper = shallowMount(Foo)
    wrapper.find('button').trigger('click')
    await flushPromises()
    expect(wrapper.vm.value).toBe('value')
  })
})

This same technique can be applied to Vuex actions, which return a promise by default.

#Using with TypeScript

An example project for this setup is available on GitHub.

TypeScript is a popular superset of JavaScript that adds types and classes on top of regular JS. Vue Test Utils includes types in the distributed package, so it works well with TypeScript.

In this guide, we'll walk through how to setup a testing setup for a TypeScript project using Jest and Vue Test Utils from a basic Vue CLI TypeScript setup.

#Adding TypeScript

First you need to create a project. If you don't have Vue CLI installed, install it globally:

$ npm install -g @vue/cli

And create a project by running:

$ vue create hello-world

In the CLI prompt, choose to Manually select features, select TypeScript, and press enter. This will create a project with TypeScript already configured.

NOTE

If you want a more detailed guide on setting up Vue with TypeScript, checkout the TypeScript Vue starter guide.

The next step is to add Jest to the project.

#Setting up Jest

Jest is a test runner developed by Facebook, aiming to deliver a battery-included unit testing solution. You can learn more about Jest on its official documentation.

Install Jest and Vue Test Utils:

$ npm install --save-dev jest @vue/test-utils

Next define a test:unit script in package.json.

// package.json
{
  // ..
  "scripts": {
    // ..
    "test:unit": "jest"
  }
  // ..
}

#Processing Single-File Components in Jest

To teach Jest how to process *.vue files, we need to install and configure the vue-jest preprocessor:

npm install --save-dev vue-jest

Next, create a jest block in package.json:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "json",
      // tell Jest to handle `*.vue` files
      "vue"
    ],
    "transform": {
      // process `*.vue` files with `vue-jest`
      ".*\\.(vue)$": "vue-jest",
    },
    "testURL": "http://localhost/"
  }
}

#Configuring TypeScript for Jest

In order to use TypeScript files in tests, we need to set up Jest to compile TypeScript. For that we need to install ts-jest:

$ npm install --save-dev ts-jest

Next, we need to tell Jest to process TypeScript test files with ts-jest by adding an entry under jest.transform in package.json:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // process `*.ts` files with `ts-jest`
      "^.+\\.tsx?$": "ts-jest"
    },
    // ...
  }
}

#Placing Test Files

By default, Jest will recursively pick up all files that have a .spec.js or .test.js extension in the entire project.

To run test files with a .ts extension, we need to change the testRegex in the config section in the package.json file.

Add the following to the jest field in package.json:

{
  // ...
  "jest": {
    // ...
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
  }
}

Jest recommends creating a __tests__ directory right next to the code being tested, but feel free to structure your tests as you see fit. Just beware that Jest would create a __snapshots__ directory next to test files that performs snapshot testing.

#Writing a unit test

Now we've got the project set up, it's time to write a unit test.

Create a src/components/__tests__/HelloWorld.spec.ts file, and add the following code:

// src/components/__tests__/HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'

describe('HelloWorld.vue', () => {
  test('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

That's all we need to do to get TypeScript and Vue Test Utils working together!

#Resources

#Using with Vue Router

#Installing Vue Router in tests

You should never install Vue Router on the Vue base constructor in tests. Installing Vue Router adds $route and $router as read-only properties on Vue prototype.

To avoid this, we can create a localVue, and install Vue Router on that.

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()

shallowMount(Component, {
  localVue,
  router
})

Note: Installing Vue Router on a localVue also adds $route and $router as read-only properties to a localVue. This means you can not use the mocksoption to overwrite $route and $router when mounting a component using a localVue with Vue Router installed.

#Testing components that use router-link or router-view

When you install Vue Router, the router-link and router-view components are registered. This means we can use them anywhere in our application without needing to import them.

When we run tests, we need to make these Vue Router components available to the component we're mounting. There are two methods to do this.

#Using stubs

import { shallowMount } from '@vue/test-utils'

shallowMount(Component, {
  stubs: ['router-link', 'router-view']
})

#Installing Vue Router with localVue

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

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

shallowMount(Component, {
  localVue
})

#Mocking $route and $router

Sometimes you want to test that a component does something with parameters from the $route and $router objects. To do that, you can pass custom mocks to the Vue instance.

import { shallowMount } from '@vue/test-utils'

const $route = {
  path: '/some/path'
}

const wrapper = shallowMount(Component, {
  mocks: {
    $route
  }
})

wrapper.vm.$route.path // /some/path

#Common gotchas

Installing Vue Router adds $route and $router as read-only properties on Vue prototype.

This means any future tests that try to mock $route or $router will fail.

To avoid this, never install Vue Router globally when you're running tests; use a localVue as detailed above.

#Using with Vuex

In this guide, we'll see how to test Vuex in components with Vue Test Utils, and how to approach testing a Vuex store.

#Testing Vuex in components

#Mocking Actions

Let’s look at some code.

This is the component we want to test. It calls Vuex actions.

<template>
  <div class="text-align-center">
    <input type="text" @input="actionInputIfTrue" />
    <button @click="actionClick()">Click</button>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default{
  methods: {
    ...mapActions([
      'actionClick'
    ]),
    actionInputIfTrue: function actionInputIfTrue (event) {
      const inputValue = event.target.value
      if (inputValue === 'input') {
        this.$store.dispatch('actionInput', { inputValue })
      }
    }
  }
}
</script>

For the purposes of this test, we don’t care what the actions do, or what the store looks like. We just need to know that these actions are being fired when they should, and that they are fired with the expected value.

To test this, we need to pass a mock store to Vue when we shallowMount our component.

Instead of passing the store to the base Vue constructor, we can pass it to a - localVue. A localVue is a scoped Vue constructor that we can make changes to without affecting the global Vue constructor.

Let’s see what this looks like:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Actions'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Actions.vue', () => {
  let actions
  let store

  beforeEach(() => {
    actions = {
      actionClick: jest.fn(),
      actionInput: jest.fn()
    }
    store = new Vuex.Store({
      state: {},
      actions
    })
  })

  it('dispatches "actionInput" when input event value is "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'input'
    input.trigger('input')
    expect(actions.actionInput).toHaveBeenCalled()
  })

  it('does not dispatch "actionInput" when event value is not "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'not input'
    input.trigger('input')
    expect(actions.actionInput).not.toHaveBeenCalled()
  })

  it('calls store action "actionClick" when button is clicked', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    wrapper.find('button').trigger('click')
    expect(actions.actionClick).toHaveBeenCalled()
  })
})

What’s happening here? First we tell Vue to use Vuex with the localVue.use method. This is just a wrapper around Vue.use.

We then make a mock store by calling new Vuex.Store with our mock values. We only pass it the actions, since that’s all we care about.

The actions are jest mock functions. These mock functions give us methods to assert whether the actions were called or not.

We can then assert in our tests that the action stub was called when expected.

Now the way we define the store might look a bit foreign to you.

We’re using beforeEach to ensure we have a clean store before each test. beforeEach is a mocha hook that’s called before each test. In our test, we are reassigning the store variables value. If we didn’t do this, the mock functions would need to be automatically reset. It also lets us change the state in our tests, without it affecting later tests.

The most important thing to note in this test is that we create a mock Vuex store and then pass it to Vue Test Utils.

Great, so now we can mock actions, let’s look at mocking getters.

#Mocking Getters

<template>
  <div>
    <p v-if="inputValue">{{inputValue}}</p>
    <p v-if="clicks">{{clicks}}</p>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default{
  computed: mapGetters([
    'clicks',
    'inputValue'
  ])
}
</script>

This is a fairly simple component. It renders the result of the getters clicks and inputValue. Again, we don’t really care about what those getters returns – just that the result of them is being rendered correctly.

Let’s see the test:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Getters from '../../../src/components/Getters'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Getters.vue', () => {
  let getters
  let store

  beforeEach(() => {
    getters = {
      clicks: () => 2,
      inputValue: () => 'input'
    }

    store = new Vuex.Store({
      getters
    })
  })

  it('Renders "state.inputValue" in first p tag', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(getters.inputValue())
  })

  it('Renders "state.clicks" in second p tag', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.findAll('p').at(1)
    expect(p.text()).toBe(getters.clicks().toString())
  })
})

This test is similar to our actions test. We create a mock store before each test, pass it as an option when we call shallowMount, and assert that the value returned by our mock getters is being rendered.

This is great, but what if we want to check our getters are returning the correct part of our state?

#Mocking with Modules

Modules are useful for separating out our store into manageable chunks. They also export getters. We can use these in our tests.

Let’s look at our component:

<template>
  <div>
    <button @click="moduleActionClick()">Click</button>
    <p>{{moduleClicks}}</p>
  </div>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'

export default{
  methods: {
    ...mapActions([
      'moduleActionClick'
    ])
  },

  computed: mapGetters([
    'moduleClicks'
  ])
}
</script>

Simple component that includes one action and one getter.

And the test:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import MyComponent from '../../../src/components/MyComponent'
import myModule from '../../../src/store/myModule'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('MyComponent.vue', () => {
  let actions
  let state
  let store

  beforeEach(() => {
    state = {
      clicks: 2
    }

    actions = {
      moduleActionClick: jest.fn()
    }

    store = new Vuex.Store({
      modules: {
        myModule: {
          state,
          actions,
          getters: myModule.getters
        }
      }
    })
  })

  it('calls store action "moduleActionClick" when button is clicked', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const button = wrapper.find('button')
    button.trigger('click')
    expect(actions.moduleActionClick).toHaveBeenCalled()
  })

  it('renders "state.inputValue" in first p tag', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(state.clicks.toString())
  })
})

#Testing a Vuex Store

There are two approaches to testing a Vuex store. The first approach is to unit test the getters, mutations, and actions separately. The second approach is to create a store and test against that. We'll look at both approaches.

To see how to test a Vuex store, we're going to create a simple counter store. The store will have an increment mutation and an evenOrOdd getter.

// mutations.js
export default {
  increment (state) {
    state.count++
  }
}
// getters.js
export default {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}

#Testing getters, mutations, and actions separately

Getters, mutations, and actions are all JavaScript functions, so we can test them without using Vue Test Utils and Vuex.

The benefit to testing getters, mutations, and actions separately is that your unit tests are detailed. When they fail, you know exactly what is wrong with your code. The downside is that you will need to mock Vuex functions, like commit and dispatch. This can lead to a situation where your unit tests pass, but your production code fails because your mocks are incorrect.

We'll create two test files, mutations.spec.js and getters.spec.js:

First, let's test the increment mutations:

// mutations.spec.js

import mutations from './mutations'

test('increment increments state.count by 1', () => {
  const state = {
    count: 0
  }
  mutations.increment(state)
  expect(state.count).toBe(1)
})

Now let's test the evenOrOdd getter. We can test it by creating a mock state, calling the getter with the state and checking it returns the correct value.

// getters.spec.js

import getters from './getters'

test('evenOrOdd returns even if state.count is even', () => {
  const state = {
    count: 2
  }
  expect(getters.evenOrOdd(state)).toBe('even')
})

test('evenOrOdd returns odd if state.count is odd', () => {
  const state = {
    count: 1
  }
  expect(getters.evenOrOdd(state)).toBe('odd')
})

#Testing a running store

Another approach to testing a Vuex store is to create a running store using the store config.

The benefit of testing creating a running store instance is we don't have to mock any Vuex functions.

The downside is that when a test breaks, it can be difficult to find where the problem is.

Let's write a test. When we create a store, we'll use localVue to avoid polluting the Vue base constructor. The test creates a store using the store-config.js export:

// store-config.spec.js

import mutations from './mutations'
import getters from './getters'

export default {
  state: {
    count: 0
  },
  mutations,
  getters
}
import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import storeConfig from './store-config'
import { cloneDeep } from 'lodash'

test('increments count value when increment is commited', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.state.count).toBe(0)
  store.commit('increment')
  expect(store.state.count).toBe(1)
})

test('updates evenOrOdd getter when increment is commited', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.getters.evenOrOdd).toBe('even')
  store.commit('increment')
  expect(store.getters.evenOrOdd).toBe('odd')
})

Notice that we use cloneDeep to clone the store config before creating a store with it. This is because Vuex mutates the options object used to create the store. To make sure we have a clean store in each test, we need to clone the storeConfig object.

#Resources

» Tiếp: API
« Trước: Các công cụ phát triển
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!