# Unit Testing
###### tags: `Vue` `test` `Jest`
## Goals of writing tests
撰寫測試的目的是什麼?大致可歸類以下三點
#### Boosted Confidence:
在一個你沒碰過的專案中,有一個測試包就像有一個前輩在隨時檢查你的code,確保你不會一新增功能就破壞程式碼。
#### Quality Code:
當你在撰寫component testing卻發現困難重重,
那可能表示你的compoent code寫得有問題,需要重構了。
因此,撰寫測試也可以確保你的code有一定的quality。
#### Better Documentation:
撰寫測試可以幫助你的團隊建立好的文件,當新的成員加入時,他可以透過測試來快速了解這個component在做什麼事。
---
## Identifying what to test
Vue app是由許多components組成,因此,最需要被測試的就是components。
### The Component Contract
每個component都會有Inputs & Outputs
**Inputs**
* Component Data
* Component Props
* User Interaction
* Ex: user clicks a button
* Lifecycle Methods
* `mounted()`, `created()`, etc.
* Vuex Store
* Route Params
**Outputs**
* What is rendered to the DOM
* External function calls
* Events emitted by the component
* Route Changes
* Updates to the Vuex Store
* Connection with children
* i.e. changes in child components
### Example: AppHeader Component
比如今天有一個header,如果`loggedIn` 是true,就display logout button
```htmlmixed
<template>
<div>
<button v-show="loggedIn">Logout</button>
</div>
</template>
<script>
export default {
data() {
return {
loggedIn: false
}
}
}
</script>
```
為了搞清楚哪些是我們要測試的,首先要找出這個component的inputs和outputs
**Inputs**
* Data (`loggedIn`)
* This data property determines if the button shows or not, so this is an input that we should be testing
**Outputs**
* Rendered Output (`button`)
* Based on the inputs (loggedIn), is our button being displayed in the DOM when it should be?
---
## What NOT to test
了解什麼是不需要測試的,可以替我們省下很多不必要的工作。
### Don’t test implementation details
只關注測試的目標是否正確產出預期的outputs,我們**不在乎它的過程或是他如何做到預期的結果**。
即便未來修改了測試目標內部的logic,只要inputs & outputs不變,我們就不需要擔心要重新撰寫測試。
### Don’t test the framework Itself
常常會犯的錯就是測試太多東西,例如測試框架有沒有正常運作。比如說compoenent裡有props的validation,像是`Number` or `Object`,不需要去測試component拿到的props是否符合這個type,因為Vue會負責這項工作。
### Don’t test third party libraries
不需要測試第三方套件是否正常運作,因為通常他們都有自己的測試,如果你不相信這個套件是否會正常運作,那就不要使用它。
---
## Writing a Unit Test with Jest
建立一個Vue專案,包含rotuer, vuex, test-utils and jest
package.json
```json
{
"name": "unit-testing-vue",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "^1.0.3",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^1.19.1",
"vue-template-compiler": "^2.6.11"
}
}
```
add ./components/AppHeader.vue
```htmlmixed
<template>
<div>
<button v-show="loggedIn">Logout</button>
</div>
</template>
<script>
export default {
data() {
return {
loggedIn: false
}
}
}
</script>
```
在tests/unit中建立測試AppHeader用的js file
./tests/unit/AppHeader.spec.js
### Identifying what to test
如同前面說的,先確認測試的inputs & outputs,剛好前面已經討論過了
**Inputs**
* Data (`loggedIn`)
* This data property determines if the button shows or not
**Outputs**
* Rendered Output (`button`) - Based on the loggedIn input, is our button displayed in the DOM or not
### Scaffolding our first unit test
Jest `describe()` function將相關聯的tests集合在一起,argument放component name的string,接著一個callback function開始寫test,run test時,console會出現component name。
```javascript
import AppHeader from '@/components/AppHeader.vue'
describe('AppHeader', () => {
})
```
<br>
根據[Jest test的文件](https://jestjs.io/docs/en/api#testname-fn-timeout),可以這樣寫
```javascript
test('a simple string that defines your test', () => {
// testing logic
}
```
這裡的test也可以改為it,Jest都認得
因此我們的test會長這樣
```javascript
describe('AppHeader', () => {
test('if user is not logged in, do not show logout button', () => {
// test body
})
test('if a user is logged in, show logout button', () => {
// test body
})
})
```
### Asserting Expectations
在Jest中,我們使用斷言集(assertions)來判斷測試的結果和預期的結果是否相符。
用Jest的`expect()`,同時也可以使用他的許多"matchers"。
e.g.
`expect(theResult).toBe(true)`
`expect()`中,放入測試的目標,接著用**matchers**來判斷result是否有符合預期。
這裡使用一個常見Jest matcher `toBe()`,裡頭放入預期的結果為true。使用斷言集可以讓讀code的人一目瞭然這個test的目的:we expect the result to be true.
在寫測試時,一開始最好先寫出一定會通過(或是一定不會通過)的初始code,以免往後在找test failed的原因時發現,根本是test本身就寫錯了。
```javascript
describe('AppHeader', () => {
test('if a user is not logged in, do not show the logout button', () => {
expect(true).toBe(true)
})
test('if a user is logged in, show the logout button', () => {
expect(true).toBe(true)
})
})
```
了解什麼matcher可以用就代表了解如何寫test了,不妨花些時間研究[the Jest matchers API](https://jestjs.io/docs/en/expect)
`npm run test`確認一下結果

<br>
### The Power of Vue Test Utils
現在可以寫入真正的logic:
1. If a user is not logged in, do not show the logout button
2. If a user is logged in, show the logout button
要在測試中確認button有沒有render,就必須要把AppHeader mounted,Vue Test Utils的library有許多packages可以做到類似的事,這裡我們需要import `mount`
```javascript
import { mount } from '@vue/test-utils'
import AppHeader from '@/components/AppHeader'
describe('AppHeader', () => {
test('if user is not logged in, do not show logout button', () => {
const wrapper = mount(AppHeader) // mounting the component
expect(true).toBe(true)
})
test('if user is logged in, show logout button', () => {
const wrapper = mount(AppHeader) // mounting the component
expect(true).toBe(true)
})
})
```
這裡命名為wrapper的原因是,除了mounting component,Vue Test Utils還創建了一個[wrapper](https://vue-test-utils.vuejs.org/api/wrapper/#wrapper),裡面有許多methods可以幫助測試。
為了要確認button是否有依照logged in value出現,使用wrapper中的兩個methods`.find()`, `.isVisible()`,`.find()`可以找出component中的button,`.isVisible()`可以確認button是否有呈現在component上。
```javascript
test('if user is not logged in, do not show logout button', () => {
const wrapper = mount(AppHeader)
expect(wrapper.find('button').isVisible()).toBe(false)
})
```
第二個情況,希望button是會出現的,因此`toBe(true)`
```javascript
test("if logged in, show logout button", () => {
const wrapper = mount(AppHeader)
expect(wrapper.find('button').isVisible()).toBe(true)
})
```
但這裡需要把loggedIn設為true,測試才會通過,[wrapper.setData()](https://vue-test-utils.vuejs.org/api/wrapper/#setdata)可以幫忙改變component的props value。
```javascript
test("if logged in, show logout button", () => {
const wrapper = mount(AppHeader)
wrapper.setData({ loggedIn: true }) // setting our data value
expect(wrapper.find('button').isVisible()).toBe(true)
})
```
現在如果開心run test 會發現竟然沒過,因為改變loggedIn value後,DOM會重新render,在還沒update完前,expect()就去找button了。
因此需要用async await來解決,確定DOM update完後,才進入expect()
```javascript
test("if logged in, show logout button", async () => {
const wrapper = mount(AppHeader)
await wrapper.setData({ loggedIn: true })
expect(wrapper.find('button').isVisible()).toBe(true)
})
```
run test後發現vue console 建議isVisible()換成Jest-dom的toBeVisible(),用法也有點不一樣。(2020/09)
官方文件說明:https://vue-test-utils.vuejs.org/upgrading-to-v1/#isvisible
[Jest-dom library](https://github.com/testing-library/jest-dom#tobevisible):
Custom jest matchers to test the state of the DOM
安裝Jest:
`npm install --save-dev @testing-library/jest-dom`
用法:
```javascript
import '@testing-library/jest-dom'
expect(warpper.find('your target').element).toBeVisible()
```
修改後的code:
```javascript
import { mount } from '@vue/test-utils'
import AppHeader from '@/components/AppHeader.vue'
import '@testing-library/jest-dom'
describe('AppHeader', () => {
test('if a user is not logged in, do not show the logout button', () => {
const wrapper = mount(AppHeader)
expect(wrapper.find('button').element).not.toBeVisible()
})
test('if logged in, show logout button', async () => {
const wrapper = mount(AppHeader)
await wrapper.setData({ loggedIn: true })
expect(wrapper.find('button').element).toBeVisible()
})
})
```
result:

---
## Testing Props & User Interaction
接下來要建立一個component,來測試傳入不同的props是否會產出預期的output以及模擬user interaction
src/components/RandomNumber.vue
```htmlmixed=
<template>
<div>
<span>{{ randomNumber }}</span>
<button @click="getRandomNumber">Generate Random Number</button>
</div>
</template>
```
```javascript=+
<script>
export default {
props: {
min: {
type: Number,
default: 1
},
max: {
type: Number,
default: 10
}
},
data() {
return {
randomNumber: 0
}
},
methods: {
getRandomNumber() {
this.randomNumber = Math.floor(Math.random() * (this.max - this.min + 1) ) + this.min;
}
}
}
</script>
```
### Identifying what to test
首先一樣釐清要測試的input & output
**Inputs**
Props:
* `min` & `max`
User Interaction:
* Clicking of the Generate Random Number button
**Outputs**
Rendered Output (DOM)
* Is the number displayed on the screen between min and max?
根據以上,可以整理三種情況需要test:
1. 沒有點buuton的情況下,預設output應為0,因為default data value = 0
2. 點button,1 <= `randomNumber` <= 10
3. props改為200 & 300,則 200 <= `randomNumber` <= 300
### Random Number Tests
build test
tests/unit/RandomNumber.spec.js
```javascript
import { mount } from '@vue/test-utils'
import RandomNumber from '@/components/RandomNumber'
describe('RandomNumber', () => {
test('By default, randomNumber data value should be 0', () => {
expect(true).toBe(false);
})
test('If button is clicked, randomNumber should be between 1 and 10', () => {
expect(true).toBe(false);
})
test('If button is clicked, randomNumber should be between 200 and 300', () => {
expect(true).toBe(false);
})
})
```
首先讓所有test failed, pass的是剛剛的AppHeader
result:

### Checking the default random number
為了確保component的default data value for randomNumber為0,一但有人更改他,就跑不過這一項測試了。
首先要mount 這個component,就可以拿到span裡的randomNumber
```javascript
test('By default, randomNumber data value should be 0', () => {
const wrapper = mount(RandomNumber)
expect(wrapper.html()).toContain('<span>0</span>')
})
```
藉由wrapper,拿到html,並判斷是否包含span with 0。
### Simulating User Interaction
接著模擬使用者click button 隨機產生1~10的number。
實作步驟:
1. mount component
2. 用`find()`取得button,用`trigger()`模擬click button,在這裏DOM會被update,因此要用async await來確保DOM update完後再繼續下一個動作。
3. 取得span中的randomNumber,並轉為type Number
4. assertion 判斷是否介於props的值之間
```javascript
test('If button is clicked, randomNumber should be between 1 and 10', async () => {
const wrapper = mount(RandomNumber)
await wrapper.find('button').trigger('click')
const randomNumber = parseInt(wrapper.find('span').element.textContent)
expect(randomNumber).toBeGreaterThanOrEqual(1)
expect(randomNumber).toBeLessThanOrEqual(10)
})
```
result:
只剩下最後一個test了

### Setting different prop values
`mount`的第二個optional argument可以傳入propsData
```javascript
test('If button is clicked, randomNumber should be between 1 and 10', () => {
const wrapper = mount(RandomNumber, {
propsData: {
min: 200,
max: 300
}
})
})
```
剩下的code就如同前一個test
```javascript
test('If button is clicked, randomNumber should be between 200 and 300', async () => {
const wrapper = mount(RandomNumber, {
propsData: {
min: 200,
max: 300
}
})
await wrapper.find('button').trigger('click')
const randomNumber = parseInt(wrapper.find('span').element.textContent)
expect(randomNumber).toBeGreaterThanOrEqual(200)
expect(randomNumber).toBeLessThanOrEqual(300)
})
```
result:

---
## Testing Emitted Events
問題:如何測試component中的emit event?
[官方Doc有說明](https://vue-test-utils.vuejs.org/api/wrapper/emitted.html)
`emitted` method 會回傳一個被wrapper emit的自定義事件(custom event)
直接用例子實作:
LoginForm.vue
```htmlmixed=
<template>
<form @submit.prevent="onSubmit">
<input type="text" v-model="name" />
<button type="submit">Submit</button>
</form>
</template>
```
```javascript=+
<script>
export default {
data() {
return {
name: ''
}
},
methods: {
onSubmit() {
this.$emit('formSubmitted', { name: this.name })
}
}
}
</script>
```
這是一個簡單的表單,點button後,會觸發submit,form綁定的onSubmit method會emit一個custom event 'formSubmitted',帶著payload user name,傳給parent component。
### Scaffolding the test file
撰寫測試時,最好能盡量模仿end user的所有操作,這裡把user的動作一一列出:
LoginForm.spec.js
```javascript
import LoginForm from '@/components/LoginForm.vue'
import { mount } from '@vue/test-utils'
describe('LoginForm', () => {
it('emits an event with a user data payload', () => {
const wrapper = mount(LoginForm)
// 1. Find text input
// 2. Set value for text input
// 3. Simulate form submission
// 4. Assert event has been emitted
// 5. Assert payload is correct
})
})
```
`it`等同`test`
### Setting the text input value
找到input然後set value
```javascript
describe('LoginForm', () => {
it('emits an event with user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('input[type="text"]') // Find text input
input.setValue('Vin') // Set value for text input
// 3. Simulate form submission
// 4. Assert event has been emitted
// 5. Assert payload is correct
})
})
```
現在只有一個input,這樣寫當然沒問題,但在production-level,一個頁面可能有好幾個input,其他人也可能更改id或class name,因此用id或class name去找input也不保險,可以用test-specific attribute解決這問題
```htmlmixed
<input data-testid="name-input" type="text" v-model="name" />
```
test file中,就可以用data-testid去找到目標input
```javascript
const input = wrapper.find('[data-testid="name-input"]')
```
<br>
```javascript
it('emits an event with a user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('[data-testid="name-input"]')
input.setValue('Vin')
// 1. Find text input
// 2. Set value for text input
// 3. Simulate form submission
// 4. Assert event has been emitted
// 5. Assert payload is correct
})
```
### Simulating the form submission
先前有用到`trigger()`來觸發click button,這裡你可能也想用一樣的方法,但有個問題,如果以後移除了button,改用keyup.enter之類其他的方法去submit form怎辦?到時test就要重新修改。因此我們直接模擬submit event
```javascript
it('emits an event with a user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('[data-testid="name-input"]')
input.setValue('Vin')
wrapper.trigger('submit')
// 4. Assert event has been emitted
// 5. Assert payload is correct
})
```
### Testing our expectations
接著就可以測試我們預期的結果:
* The event has been emitted
* The payload is correct
```javascript
it('emits an event with a user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('[data-testid="name-input"]') // Find text input
input.setValue('Vin') // Set value for text input
wrapper.trigger('submit') // Simulate form submission
// Assert event has been emitted
const formSubmittedCalls = wrapper.emitted('formSubmitted')
expect(formSubmittedCalls).toHaveLength(1)
})
```
formSubmitted是component中emit的custom event,根據文件,`wrapper.emitted('formSubmitted')`會回傳一個array(也可以寫作`wrapper.emitted().formSubmitted`),因此判斷array是否length === 1
console.log(formSubmittedCalls)會長這樣:`[ [ { name: 'Vin' } ] ]`
接著要確認這個event emit的payload是否符合user name。
target payload: `formSubmittedCalls[0][0]`
完整的test code:
```javascript
import LoginForm from '@/components/LoginForm.vue'
import { mount } from '@vue/test-utils'
describe('LoginForm', () => {
it('emits an event with a user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('[data-testid="name-input"]') // Find text input
input.setValue('Vin') // Set value for text input
wrapper.trigger('submit') // Simulate form submission
// Assert event has been emitted
const formSubmittedCalls = wrapper.emitted('formSubmitted')
expect(formSubmittedCalls).toHaveLength(1)
// Assert payload is correct
const expectedPayload = { name: 'Vin' }
expect(formSubmittedCalls[0][0]).toMatchObject(expectedPayload)
})
})
```
---
## Testing API Calls
除非你的Vue專案都只有靜態檔案,否則通常都會需要用到API Calls,在component中測試API Calls時,不希望真的發出請求給後端伺服器,因為這樣做會造成測試code和後端直接連結,容易出現不確定性,且降低測試速度。因此用[Jest mock function](https://jestjs.io/docs/en/mock-functions.html)可以模擬 fetching api calls的過程,並且提供一些methods作為測試使用。
### The Starting Code
在這個例子中,使用[json-server](https://github.com/typicode/json-server)來做後端資料庫的server,對於不複雜的資料結構來說很夠用。
./db.json
```javascript
{
"message": { "text": "Hello from the db!" }
}
```
在根目錄中建立db.json,就可以用`json-server --watch db.json`來建立一個server,用來回傳db.json裡的data。
./services/axios.js
```javascript
import axios from 'axios'
export function getMessage() {
return axios.get('http://localhost:3000/message').then(response => {
return response.data
})
}
```
axios.js會export getMessage(),呼叫這個function,他就會用get mothod跟db.json server拿到message。
接著建立呼叫這個API call的component
MessageDisplay.vue
```htmlmixed=
<template>
<p v-if="error" data-testid="message-error">{{ error }}</p>
<p v-else data-testid="message">{{ message.text }}</p>
</template>
```
```javascript=+
<script>
import { getMessage } from '@/services/axios.js'
export default {
data() {
return {
message: {},
error: null
}
},
async created() {
try {
this.message = await getMessage()
} catch (err) {
this.error = 'Oops! Something went wrong.'
}
}
}
</script>
```
當component `created`,會用getMessage()去拿到message,並呈現在view上,如果error發生,也會有對應的message。
### Inputs & Outputs
從`getMessage()`取得的response是input,output有兩種情況:
1. The call happens successfully and the message is displayed
1. The call fails and the error is displayed
所以在test中,要做的是:
1. Mock a successful call to getMessage, checking that the `message` is displayed
2. Mock a failed call to getMessage, checking that the `error` is displayed
### Mocking Axios
用comment寫下每一個步驟
MessageDisplay.spec.js
```javascript
import MessageDisplay from '@/components/MessageDisplay'
import { mount } from '@vue/test-utils'
describe('MessageDisplay', () => {
it('Calls getMessage and displays message', async () => {
// mock the API call
const wrapper = mount(MessageDisplay)
// wait for promise to resolve
// check that call happened once
// check that component displays message
})
it('Displays an error when getMessage call fails', async () => {
// mock the failed API call
const wrapper = mount(MessageDisplay)
// wait for promise to resolve
// check that call happened once
// check that component displays error
})
})
```
為了mock API call,先把`getMessage`從axios.js import進來。
將axios.js的路徑傳給`jest.mock()`,便能mock `getMessage()`,以及使用jest的mathods
```javascript
import MessageDisplay from '@/components/MessageDisplay'
import { mount } from '@vue/test-utils'
import { getMessage } from '@/services/axios'
jest.mock('@/services/axios')
...
```
可以將`jest.mock` 想作:我拿了你的`getMessage` function,然後回傳給你一個mocked `getMessage` function
因此,當我們之後呼叫`getMessage`,其實我們用的是mocked的`getMessage`,並不是真的`getMessage`
```javascript
import MessageDisplay from '@/components/MessageDisplay'
import { mount } from '@vue/test-utils'
import { getMessage } from '@/services/axios'
jest.mock('@/services/axios')
describe('MessageDisplay', () => {
it('Calls getMessage and displays message', async () => {
const mockMessage = 'Hello from the db'
getMessage.mockResolvedValueOnce({ text: mockMessage }) // calling our mocked get request
const wrapper = mount(MessageDisplay)
// wait for promise to resolve
// check that call happened once
// check that component displays message
})
})
```
[`mockResolvedValueOnce`](https://jestjs.io/docs/en/mock-function-api.html#mockfnmockresolvedvalueoncevalue)表示:模擬一個回傳的value,讓這個mocked API call可以resolve,所以這裡我們傳入預期的text,也就是mockMessage。
到這裡為止,我們成功模擬了一個API call,又模擬了這個API call回傳的值,而且完全沒有和server有任何關聯。
如果你有注意到這裡已經先寫了`async`,因為axios是asynchronous,必須確定所有mocked call return 的promise都完成resolved,才能去寫assertion,否則一定fail。
### Awaiting Promises
在搞懂哪裡要寫await之前,要先想:component中的`getMessage`是如何被呼叫的?
MessageDisplay.vue
```javascript
async created() {
try {
this.message = await getMessage()
} catch (err) {
this.error = 'Oops! Something went wrong.'
}
}
```
確認了是在`created`階段被呼叫的,但是vue-test-utils並沒有辦法可以取得`created`階段的promises,因此必須借助一個套件[flush-promises](https://www.npmjs.com/package/flush-promises),這個套件可以確保所有的promises都resolved才進行下一個task。
補充:官方文件[這裡](https://vue-test-utils.vuejs.org/guides/#testing-asynchronous-behavior)有寫測試非同步的教學
<br>
MessageDisplay.spec.js
```javascript
import MessageDisplay from '@/components/MessageDisplay'
import { mount } from '@vue/test-utils'
import { getMessage } from '@/services/axios'
import flushPromises from 'flush-promises'
jest.mock('@/services/axios')
describe('MessageDisplay', () => {
it('Calls getMessage once and displays message', async () => {
const mockMessage = 'Hello from the db'
getMessage.mockResolvedValueOnce({ text: mockMessage })
const wrapper = mount(MessageDisplay)
await flushPromises()
// check that call happened once
// check that component displays message
})
})
```
### Our Assertions
首先,確保我們沒有對server做出多次request
```javascript
it('Calls getMessage once and displays message', async () => {
const mockMessage = 'Hello from the db'
getMessage.mockResolvedValueOnce(mockMessage)
const wrapper = mount(MessageDisplay)
await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1) // check that call happened once
// check that component displays message
})
```
使用`.toHaveBeenCalledTimes()`並傳入數字1。
接著,我們要拿到component裡,透過`getMessage()` request拿到的text,然後比對看看是否和mockMessage相同。
在MessageDisplay.vue裡已經寫好讓test使用的id: `data-testid="message"`
MessageDisplay.spec.js
```javascript
it('Calls getMessage once and displays message', async () => {
const mockMessage = 'Hello from the db'
getMessage.mockResolvedValueOnce({ text: mockMessage })
const wrapper = mount(MessageDisplay)
await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)
const message = wrapper.find('[data-testid="message"]').element.textContent
expect(message).toEqual(mockMessage)
})
```
此時run test,終於過了!
接著就是測試API call failed的情況。
### Mocking a failed request
第一步,mocking a failed API call,和前面的test很類似
```javascript
it('Displays an error when getMessage call fails', async () => {
const mockError = 'Oops! Something went wrong.'
getMessage.mockRejectedValueOnce(mockError)
const wrapper = mount(MessageDisplay)
await flushPromises()
// check that call happened once
// check that component displays error
})
```
不同處在於mockError以及`getMessage().mockRejectValueOnce()`,既然是failed,用reject也是理所當然的。
awaiting flushPromises()後,就可以去判斷component呈現的訊息是否和mockError相同了
```javascript
it('Displays an error when getMessage call fails', async () => {
const mockError = 'Oops! Something went wrong.'
getMessage.mockRejectedValueOnce(mockError)
const wrapper = mount(MessageDisplay)
await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)
const displayedError = wrapper.find('[data-testid="message-error"]').element
.textContent
expect(displayedError).toEqual(mockError)
})
```
現在run test會出現fail log:
**Expected number of calls: 1 Received number of calls: 2**
原來是前面第一個test已經call `getMessage()`,現在第二個test又呼叫一次,所幸jest有辦法清除mock actions
### Clear All Mocks
在jest.mock()的下方寫入解法
```javascript=
jest.mock('@/services/axios')
beforeEach(() => {
jest.clearAllMocks()
})
```
現在`beforeEach` test,jest都會把所有的mocks清除,再run test便成功了。

---
## Stubbing Child Components
當你想測試一個component,它的裡頭還有一個child component,且這個child component還調用了一些外部服務(像是axios),我們的目的是測試這個component,child component的事應該要另外做它自己的測試,這時候就需要把child component [stubbing](https://lmiller1990.github.io/vue-testing-handbook/stubbing-components.html#stubbing-components)
### Children with Baggage
為了瞭解stubbing component的概念,先建立一個MessageContainer.vue
```htmlmixed=
<template>
<MessageDisplay />
</template>
```
```javascript=+
<script>
import MessageDisplay from '@/components/MessageDisplay'
export default {
components: {
MessageDisplay
}
}
</script>
```
**MessageContainer**僅僅只是import **MessageDisplay**,這也就是前面測試過的**MessageDisplay**,所以當**MessageContainer**渲染的時候,**MessageDisplay**也跟著被渲染,然後調用了axios,也就是剛剛才提到的問題:不希望真的去呼叫**MessageDisplay**中`created` hook會觸發的axios `get` request。
MessageDisplay.vue
```javascript
async created() {
try {
this.message = await getMessage() // Don't want this to fire in parent test
} catch (err) {
this.error = 'Oops! Something went wrong.'
}
}
```
所以該如何對**MessageContainer**測試且不會觸發child component的axios `get` request?
其實在這個例子中,只有一個外部服務(or you can call module dependency),我們可以直接mock axios,就像**MessageDisplay.spec.js**裡寫的測試一樣,但萬一child component調用了多組外部服務,就絕對不能如法炮製。這時就會直接mock child component本身,而不去mock它的相關依賴或套件,也就是用**stub**,或者說一個假的、替代版本的child component。
### The MessageContainer Test
MessageContainer.spec.js
```javascript=
import MessageContainer from '@/components/MessageContainer'
import { mount } from '@vue/test-utils'
describe('MessageContainer', () => {
it('Wraps the MessageDisplay component', () => {
const wrapper = mount(MessageContainer)
})
})
```
在這裡該如何stub掉**MessageDisplay**?還記得axios是在`created` hook時被呼叫的,因此,我們要在**MessageContainer** mount **MessageDisplay** 前阻止這件事。
這樣一想,在mount(MessageContainer)後面插入一個argument `stubs`就很合理了。([see doc here](https://vue-test-utils.vuejs.org/api/options.html#stubs))
```javascript
import MessageContainer from '@/components/MessageContainer'
import MessageDisplay from '@/components/MessageDisplay.vue'
import { mount } from '@vue/test-utils'
describe('MessageContainer', () => {
it('Wraps the MessageDisplay component', () => {
const wrapper = mount(MessageContainer, {
stubs: {
MessageDisplay: MessageDisplay
}
})
expect(wrapper.findComponent(MessageDisplay).exists()).toBe(true)
})
})
```
先把**MessageDisplay** import進來,根據[文件說明](https://lmiller1990.github.io/vue-testing-handbook/stubbing-components.html#write-a-test-using-mount),設定stubs,接著寫assertion,只要確定**MessageContainer**有包含**MessageDisplay**就好了,畢竟在這個例子中**MessageContainer**也只有做這件事。
### The Disadvantages of Stubbing
* **maintenance costs**: 由於stub是替代性的code,當真正的code有變動的時候,也需要同步調整test裡的stub,可能會增加維護的成本。
* **reduced confidence**: stub不是一個真的rendered component,會減少你對真正的component的測試覆蓋率,可能導致你對於測試是否真正能夠反應web app的問題產生疑問。
### What about ShallowMount?
可能很常看到有人使用`shallowMount`,也查到文件說`shallowMount`只會mount最上層component,但前面都沒有使用的原因是?
1. stubs的問題他全部都有
2. 假設你用了一些和Vue Test Utils有關的library,e.g. [Vue Testing Library](https://testing-library.com/docs/vue-testing-library/intro),你會發現`shallowMount`完全不支援這些library。
[For more information of avoid using shallowMount](https://kentcdodds.com/blog/why-i-never-use-shallow-rendering)