Vite Vue 單元測試

describe:是用來將一至多組有相關的測試組合在一起的區塊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { describe, it} from 'vitest'
import { mount } from "@vue/test-utils"

const number = 2

describe('Number', () => {
test('is 2', () => {
expect(number).toBe(2)
})

test('is even', () => {
expect(number % 2).toBe(0)
})
})
  • attributes:判斷屬性。
  • class:尋找 class 屬性
    target:尋找target 屬性
    1
    2
    3
    4
    <template>
    <a data-test="link" href="https://ithelp.ithome.com.tw/" target="_blank">ithelp</a>
    </template>

    單元測試結果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
        /引入描述、它、期望
    import { describe, it, expect,expectTypeOf } from 'vitest'
    //引入測試實用程式
    import { mount } from '@vue/test-utils'
    //引入元件
    import AButton from '../A.vue'

    describe('A標籤', () =>
    {
    const href = 'https://ithelp.ithome.com.tw/'
    const target = '_blank'

    let wrapper
    if (typeof document !== 'undefined') {
    wrapper = mount(AButton, {
    props: {
    href: href,
    target: target
    }
    })
    }
    it('取得A標籤所有訊息,A標籤樣式是否包含link', () =>
    {
    if (typeof document !== 'undefined') {
    wrapper.find('[data-test="link"]').attributes()
    console.log('取得所有A標籤訊息', wrapper.find('[data-test="link"]').attributes())
    // 屬性{ href: 'https://ithelp.ithome.com.tw/', 'target': '_blank' }
    expect(wrapper.find('[data-test="link"]').classes()).toContain('link')
    // class樣式link
    }
    })
    })
    <div><b>classes:</b>語法查詢的話將得到一個陣列的結果</div>
      <div>
      
    1
    wrapper.find('[data-test="wrap"]').classes()
    </div> </li> <li><b>expect(value):</b>攥寫每一筆測試時都會使用 expect(value) 和匹配器 (matcher) 來斷言某個值,expect (value) 的參數 value 會是程式碼產生的值</li> <li><b> toBe </b>是一個匹配器</li> <li><b> html() </b>回傳元件的 HTML</li> <li><b> toContain() </b>檢查一個字符串是否是另一個字符串的子字符串,也可檢查一個項目是否在 Array 中。</li> <li><b> get() </b> get 方法來搜索現有元素:如果 get() 沒有找到目標元素,它會拋出錯誤並導致測試失敗。如果找到的話則會回傳一個 DOMWrapper。</li> <li><b>find()</b>和 get() 很像,一樣是使用 Document.querySelector() 的語法,不過差別在於 find() 沒有找到目標元素時不會拋出錯誤。</li> <li><b>exist()</b> 檢查元素是否存在。</li> <li><b> text() </b> 回傳元素的文本內容 (text content)。</li> <li><b>isVisible ()</b>專門用來檢查元素是否為隱藏的狀態:元素或元素的祖先中有 display: none、visibility: hidden、opacity:0 的樣式,收合的 <details> 標籤中,具有 hidden 屬性</li> <li><b>trigger ()</b>可以用來觸發 DOM 事件,例如 click、submit 或 keyup 等操作,值得注意的是, trigger 回傳的是一個 Promise,也因此我使用了 async & await 的方式來等待 promise resolve。</li> <li><b>emits()</b>事件通常是由子元件向父元件所觸發的</li> <li><b>vm</b>是 VueWrapper 的一個屬性,我們可以透過 vm 來取得 Vue instance,如此一來就能再透過 vm 取得 count 變數了。</li> <li><b>emitted() </b>emitted() 會回傳一個紀錄元件發出的所有事件的物件,其中也包含著 emit 的參數。</li> <li><b>toHaveProperty()</b>jest 有提供一個 toHaveProperty 的匹配器 (matcher),可以用來檢查物件中是否存在某屬性。</li> <li><b>toEqual()</b>匹配器會去比較物件的所有屬性或陣列的所有元素是否相等。</li>

mount:透過 mount() 產生一個已經掛載 (mounted) 和渲染完的元件(Wrapper),並對其進行操作和斷言。

mount的第二個參數是可以用來定義元件的狀態 (state) 配置,例如 props, data, attrs 等等,因此這次我們就傳入 data 覆蓋掉元件中的預設值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'

describe('HelloWorld', () =>
{
//1+1 應該是2
it('1 + 1 should be 2', () => {
expect(1 + 1).toBe(2)
})
//
it('正確渲染', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
it('在此測試檔案中使用 jsdom', () => {
const element = document.createElement('div')
element.innerHTML = '<p>Hello, HTML!</p>'
expect(element.innerHTML).toBe('<p>Hello, HTML!</p>')
})
})

shallowMount():透過 shallowMount 產生的 wrapper 元件,如果它有子元件的話,子元件不會被解析渲染,也不會觸發子元件內的程式碼,而是會用 stub 來替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { mount, shallowMount } from '@vue/test-utils'

const Child = {
template: "<div>Child component</div>"
}

const Parent = {
template: "<div><child /></div>",
components: {
Child
}
}

describe('shallowMount example', () => {
test('test', () => {
const childShallowWrapper = shallowMount(Child)
const childMountWrapper = mount(Child)
console.log(childShallowWrapper.html())
console.log(childMountWrapper.html())

const parentShallowWrapper = shallowMount(Parent)
const parentMountWrapper = mount(Parent)
console.log(parentShallowWrapper.html())
console.log(parentMountWrapper.html())
})
})

執行結果

善用data-test

Unit test透過 get() 或 find() 來尋找目標的元素,在元件內綁定元件

1
2
3
4
5
6
7
8
9
10
11
12
import { mount } from '@vue/test-utils'

const Component = {
template: '<div data-test="target">dataset</div>'
}

test('render dataset', () => {
const wrapper = mount(Component)

expect(wrapper.get('[data-test="target"]').text()).toBe('dataset')
})

表單單元測試

1
2
3
4
5
6
7
8
9
10
<template>
<div>
<input type="email" v-model="email" data-test="email" />
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const email = ref<string>('')
</script>

測試=>輸入元素的值應該是 my@mail.com

1
2
3
4
5
6
7
8
test('輸入元素的值應該是 my@mail.com', async () => {
const wrapper = mount(Component)
const input = wrapper.get('[data-test="email"]')

await input.setValue('my@mail.com')

expect(input.element.value).toBe('my@mail.com')
})
  • setValue()要更改表單元素的值可以使用 setValue() 方法,setValue 接受一個參數,可以是字串或布林值,並且回傳的是一個 Promise。
  • DOMWrapper透過 get() 或是 find() 成功找到目標元素時都會回傳一個圍繞 Wrapper API 的 DOM 元素的瘦包裝器 (thin wrapper),而它有一個代表著 HTMLElement 的屬性 element,又因為在上面的情況目標元素為 input tag 所以此時 element 真實的值其實為 HTMLInputElement。

測試 =>設定值後,電子郵件的值應為 my@mail.com

1
2
3
4
5
6
7
test('設定值後,電子郵件的值應為 my@mail.com', async () => {
const wrapper = mount(Component)

await wrapper.get('[data-test="email"]').setValue('my@mail.com')

expect(wrapper.vm.email).toBe('my@mail.com')
})

比較複雜的表單

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<form data-test="form" @submit.prevent="submit">
<input data-test="email" type="email" v-model="form.email" />

<textarea data-test="description" v-model="form.description" />

<select data-test="city" v-model="form.city">
<option value="taipei">Taipei</option>
<option value="tainan">Tainan</option>
</select>

<input data-test="subscribe" type="checkbox" v-model="form.subscribe" />

<input data-test="interval.weekly" type="radio" value="weekly" v-model="form.interval" />
<input data-test="interval.monthly" type="radio" value="monthly" v-model="form.interval" />

<button type="submit"
@click="sendSubmit"
>Submit</button>
</form>
</template>

<script setup lang="ts">
const props = defineProps({
form:{type:Object}
})
const emit = defineEmits(['sendSubmit'])
const sendSubmit=()=>{
emit('sendSubmit')
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<Forms
:form="form"
@sendSubmit ="submit"
></Forms>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Forms from '../component/form.vue'
interface formType{
email: string,
description: string,
city: string,
subscribe: boolean,
interval: string,
}
const form = ref<formType>({
email: '',
description: '',
city: '',
subscribe: false,
interval: ''
})
const submit = () => {
emit('submit', form)
}
</script>

表單測試
Case 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
test('填寫表單', async () => {
const wrapper = mount(Component)

const email = 'name@mail.com'
const description = 'Lorem ipsum dolor sit amet'
const city = 'taipei'
const subscribe = true

await wrapper.get('[data-test="email"]').setValue(email)
await wrapper.get('[data-test="description"]').setValue(description)
await wrapper.get('[data-test="city"]').setValue(city)
await wrapper.get('[data-test="subscribe"]').setValue()
await wrapper.get('[data-test="interval.weekly"]').setValue()

expect(wrapper.vm.form).toEqual({
email,
description,
city,
subscribe,
interval: 'weekly'
})
})

Case 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
test('提交表單', async () => {
const wrapper = mount(Component)

const email = 'name@mail.com'
const description = 'Lorem ipsum dolor sit amet'
const city = 'taipei'
const subscribe = true

await wrapper.get('[data-test="email"]').setValue(email)
await wrapper.get('[data-test="description"]').setValue(description)
await wrapper.get('[data-test="city"]').setValue(city)
await wrapper.get('[data-test="subscribe"]').setValue(subscribe)
await wrapper.get('[data-test="interval.monthly"]').setValue()

await wrapper.get('[data-test="form"]').trigger('submit.prevent')

expect(wrapper.emitted('submit')[0][0]).toEqual({
email,
description,
city,
subscribe,
interval: 'monthly'
})
})
  • emitted()會回傳一個紀錄元件發出的所有事件的物件,其中也包含著 emit 的參數

prop & Computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { ref, computed } from 'vue'

const Component = {
template: `
<div>
<input data-test="password" v-model="password">
<p v-if="showError && isError" data-test="errorMsg">Password must be at least {{minLength}} characters.</p>
</div>
`,
props: {
minLength: {
type: Number,
required: true
},
showError: {
type: Boolean,
default: true
}
},
setup (props) {
const password = ref('')
const isError = computed(() => password.value.length < props.minLength)

return {
isError,
password
}
}
}
  • beforeEach() 在每一個 test() 執行前運行的一個函式,常會用來初始化 wrapper 。
  • setProps() 在 wrapper 生成後,動態的改變 props 的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
describe('Props & Computed', () => {
let wrapper
const minLength = 6
beforeEach(() => {
wrapper = mount(Component, {
props: {
minLength
}
})
})

// Case 1: 密碼在大於或等於最短長度限制時,不會出現錯誤訊息。
test(`not renders an error if length is more than or equal to ${minLength}`, async () => {
await wrapper.get('[data-test="password"]').setValue('123456')

expect(wrapper.find('[data-test="errorMsg"]').exists()).toBe(false)
})

// Case 2: 密碼少於最短長度限制時,出現錯誤訊息。
test(`renders an error if length is less than ${minLength}`, async () => {
await wrapper.get('[data-test="password"]').setValue('12345')

expect(wrapper.html()).toContain(`Password must be at least ${minLength} characters`)
})

// Case 3: 當 showError 為 false 時,不顯示錯誤訊息。
test('not renders an error if showError is false ', async () => {
await wrapper.get('[data-test="password"]').setValue('12345')

expect(wrapper.html()).toContain(`Password must be at least ${minLength} characters`)

await wrapper.setProps({ showError: false })

expect(wrapper.find('[data-test="errorMsg"]').exists()).toBe(false)
})
})