---
# System prepended metadata

title: Unit Testing
tags: [test, Jest, Vue]

---

# 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`確認一下結果

![](https://i.imgur.com/Tigc84C.png)

<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:

![](https://i.imgur.com/oAFMKuI.png)

---

## 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:

![](https://i.imgur.com/dhGoK5v.png)


### 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了
![](https://i.imgur.com/JlhG0uX.png)


### 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:
![](https://i.imgur.com/HMJcHDa.png)


---

## 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便成功了。

![](https://i.imgur.com/0RLHKPG.png)

---

## 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)