單元測試 describe & it

describe & it 輔助 API

describe

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

describe('登入元件', () => {
// Happy Path
it('輸入正確帳號密碼,應登入到OO頁面', () => {
// ...
})
// Sad Path
it('只輸入帳號,應該顯示請輸入密碼', () => {
// ...
})
it('只輸入密碼,應該顯示請輸入帳號', () => {
// ...
})
it('輸入錯誤帳號密碼,應該顯示登入資訊錯誤', () => {
// ...
})
})

允許巢狀的方式來建構測試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
describe('父層情境', () => {
describe('基於父層情境的情境一', () => {
it('測試案例', () => {
// ...
})
})
describe('基於父層情境的情境二', () => {
it('測試案例', () => {
// ...
})
it('出現提示寄件者姓名與電話將直接註冊成會員', () => {
// ...
})
})
})

describe & it 輔助 API

  • .only:測試情境、測試案例皆可使用
  • .skip:測試情境、測試案例皆可使用
  • .todo:測試情境、測試案例皆可使用
  • .fails:測試案例才能使用
only
若在測試情境用了此指令,則在同個測試檔案中只會執行帶有 .only 的測試情境,而其餘測試情境底下所有的測試案例將會被跳過(skipped):
1
2
3
describe.only('測試情境 1', () => { /* */ })
describe('測試情境 2', () => { /* */ }) // skipped
describe('測試情境 3', () => { /* */ }) // skipped
若在測試案例中使用,則除了帶有 .only 之外的測試案例都將會被跳過:
1
2
3
4
5
6
7
8
9
10
11
describe('測試情境 1', () => {
it.only('測試案例', () => { /* */ })
it('測試案例', () => { /* */ }) // skipped
})

describe('測試情境 2', () => {
it.only('測試案例', () => { /* */ })
it('測試案例', () => { /* */ }) // skipped
})

describe('測試情境 3', () => { /* */ }) // skipped

skip

測試情境或測試案例被標注時,將自動跳過該範疇內的測試案例:
1
2
3
4
5
6
7
8
9
describe.skip('測試情境 1', () => {
it('測試案例', () => { /* */ }) skipped
it('測試案例', () => { /* */ }) // skipped
})

describe('測試情境 2', () => {
it('測試案例', () => { /* */ })
it.skip('測試案例', () => { /* */ }) // skipped
})

todo

測試情境或測試案例被標注時,同樣將自動跳過該範疇內的測試案例,但 todo 含義比較接近待加入測試的區塊,並且將來若產出報告時也會特別整理出還有哪些地方需要補上測試。

fails

最後一個介紹的是測試案例才能使用的輔助 API,還記得列測試案例時的 sad path 嗎?當測試案例應該要失敗的時候就可以透過 fails 顯性標註他們:
1
2
3
4
it.fails(`'1' + '1' should not to be '11'`, () => {
const add = (x, y) => Number(x) + Number(y)
expect(add('1', '1')).toBe('11')
})
當然你也可以單純藉由斷言中的 .not 達到同樣的效果:
1
2
3
4
it(`'1' + '1' should not to be '11'`, () => {
const add = (x, y) => Number(x) + Number(y)
expect(add('1', '1')).not.toBe('11')
})
準備(Setup)與清理(Teardown)
  • beforeEach:在每個測試案例執行前呼叫一次
  • beforeAll:在所有測試案例執行前呼叫一次
  • afterEach:在每個測試案例執行後呼叫一次
  • afterAll:在所有測試案例執行後呼叫一次
1
2
3
4
beforeEach(() => {
// 針對測試案例重新初始化
initTestEnv()
})
Setup & Teardown API 的範疇
Setup & Teardown API 「所有」的定義是根據當下的範疇(context)來決定,除了測試檔案本身之外,使用 describe 來定義測試情境也會形成一個 context,因此假如測試情境有巢狀的情況如下:
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const history = []
describe('父層情境', () => {
beforeAll(() => {
history.push('beforeAll - 父層情境')
})
beforeEach(() => {
history.push('beforeEach - 父層情境')
})
afterAll(() => {
history.push('afterAll - 父層情境')
})
afterEach(() => {
history.push('afterEach - 父層情境')
})
describe('子層情境 A', () => {
beforeAll(() => {
history.push('beforeAll - 子層情境 A')
})
beforeEach(() => {
history.push('beforeEach - 子層情境 A')
})
afterAll(() => {
history.push('afterAll - 子層情境 A')
})
afterEach(() => {
history.push('afterEach - 子層情境 A')
})
it('案例 1', () => {
history.push('子層情境 A 案例 1')
})
it('案例 2', () => {
history.push('子層情境 A 案例 2')
})
})
describe('子層情境 B', () => {
beforeAll(() => {
history.push('beforeAll - 子層情境 B')
})
beforeEach(() => {
history.push('beforeEach - 子層情境 B')
})
afterAll(() => {
history.push('afterAll - 子層情境 B')
})
afterEach(() => {
history.push('afterEach - 子層情境 B')
})
it('案例 1', () => {
history.push('子層情境 B 案例 1')
})
it('案例 2', () => {
history.push('子層情境 B 案例 2')
})
})
})

此時將透過 console.log(history) 查看並歸納整理就能得到以下結果:

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
--- 進入測試程式碼本身的 Context
--- 進入父層情境的 Context
beforeAll - 父層情境
--- 進入子層情境 A 的 Context
beforeAll - 子層情境 A
beforeEach - 父層情境
beforeEach - 子層情境 A
子層情境 A 案例 1 // 執行 情境 A 案例 1 的時間點
afterEach - 子層情境 A
afterEach - 父層情境
beforeEach - 父層情境
beforeEach - 子層情境 A
子層情境 A 案例 2 // 執行 情境 A 案例 2 的時間點
afterEach - 子層情境 A
afterEach - 父層情境
afterAll - 子層情境 A
--- 離開子層情境 A 的 Context

--- 進入子層情境 B 的 Context
beforeAll - 子層情境 B
beforeEach - 父層情境
beforeEach - 子層情境 B
子層情境 B 案例 1 // 執行 情境 B 案例 1 的時間點
afterEach - 子層情境 B
afterEach - 父層情境
beforeEach - 父層情境
beforeEach - 子層情境 B
子層情境 B 案例 2 // 執行 情境 B 案例 2 的時間點
afterEach - 子層情境 B
afterEach - 父層情境
afterAll - 子層情境 B
--- 離開子層情境 B 的 Context
afterAll - 父層情境
--- 離開父層情境的 Context
--- 離開測試程式碼本身的 Context
避免誤區:在 expect 後做清掃處理
除了上面的用法,有時候你可能會認為既然要清掃,那我何不在斷言後處理就好呢:
1
2
3
4
5
6
7
describe('', () => {
it('', () => {
expect().toBe()
// 在這裡做清除
resetTestingEnv()
})
})
這麼做當你在測試案例都是通過的情況下都沒有問題,但是一但某個測試案例發生了錯誤,由於測試案例就會在斷言時拋出 AssertionError 後停止,因此很有可能因為一個測試案例壞了導致接下來所有測試都受到影響:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
describe('', () => {
it('', () => {
expect().toBe() // AssertionError,這個測試案例就停在這了
resetTestingEnv()
})
it('', () => {
// 在沒有經過 `resetTestingEnv()` 下進行測試
})
it('', () => {
// 在沒有經過 `resetTestingEnv()` 下進行測試
})
it('', () => {
// 在沒有經過 `resetTestingEnv()` 下進行測試
})
it('', () => {
// 在沒有經過 `resetTestingEnv()` 下進行測試
})
})
因此較佳的作法還是使用 Setup & Teardown API 來處理會比較好:
1
2
3
4
5
6
7
8
9
10
11
12
13
describe('', () => {
beforeEach('', () => {
setupTestingEnv()
})
afterEach('', () => {
resetTestingEnv()
})
it('', () => {})
it('', () => {})
it('', () => {})
it('', () => {})
it('', () => {})
})