在開始撰寫測試之前,先帶大家來了解一下 Angular 預設使用的測試框架 ─ Karma 。
Karma 的原名是 Testacular , Google 在 2012 年的時候將其開源, 2013 年時將其改名為 Karma ,它是基於 Jasmine 與 Selenium 所開發出來的 JavaScript 測試執行過程管理工具(Test Runner)。
一般我們會使用它來撰寫單元測試與整合測試,測試的檔案名稱通常會命名為 xxx.spec.ts
,而只要是使用 Angular CLI 所建立的檔案,在預設的情況下都會連帶產生該檔案,像是: xxx.component.spec.ts
、 xxx.service.spec.ts
。
當我們想要執行測試程式時,只要使用指令 npm test
or yarn test
or ng test
,就可以看到它的執行結果:
當 Karma 執行起來後,只要我們不停掉它的 server 且不關掉它的視窗,只要我們有修改我們的測試並存檔後,它就會偵測到我們的變動並再重新跑一次測試,是個很方便且強大的功能。
關於執行測試時的更多參數,請參考Angular 官方 API 文件
想了解更多的話,可參考網路文章:JavaScript 測試工具之 Karma-Jasmine 的安裝和使用詳解與 Karma 官方文件
上述提到,在 Angular 裡的測試檔案一般我們會將其命名為 xxx.spec.ts
,而檔案內容大致上會長這樣:
或是這樣:
從中我們可以發現,它是一種巢狀式的結構,外層會是一個名字叫 describe
的函式,內層則有許多名為 it
的函式,這些函式各是什麼意思呢?
it
指的是 測試案例(Test case),通常會在 describe
函式的裡面,使用方式如下所示:
it('說明文字', () => {
// test content
});
第一個參數是該測試案例的說明文字,讓我們在閱讀時可以很清楚、直接地知道這個測試案例會有什麼結果,通常建議以 should
做開頭,整體閱讀起來較為順暢,例如:
it('should be created', () => {
// test content
});
或者像是:
it('should have as title "Angular"', () => {
// test content
});
第二個參數是一個函式,裡面就是該測試案例所要執行的程式碼,也就是我們實際上要測試的內容。
describe
指的是 測試集合(Test suite),主要是用於將測試案例分組、分類,類似資料夾的概念,這樣我們在閱讀程式碼的時候與其測試結果時,才會比較好閱讀
使用方式如下所示:
describe('說明文字', () => {
// test cases
});
跟 it
一樣,第一個參數是該測試集合的說明文字,讓我們在閱讀時可以很清楚、直接地知道這個測試集合的主要測試目標,例如:
describe('LoginComponent', () => {
describe('Component logic', () => {
describe('login', () => {
// test cases
});
});
describe('Template logic', () => {
describe('When login button be clicked', () => {
// test cases
});
});
});
第二個參數是一個函式,裡面是該測試集合所要執行的測試案例。
describe
除了分類、分組的功能外,他還有一個很重要的特性 ─ 作用域(Scoping) 。
在寫測試案例的時候,我們可能會遇到某些情況是在需要事先做一些配置,又或者是驗證完之後需要把某些狀態還原,如果將這些事情寫在每一個 it
裡又覺得很囉嗦且不好維護,這時候我們就會使用以下這些函式來幫我們:
beforeAll
─ 在執行所有的測試案例之前,會先執行這裡面的程式碼。beforeEach
─ 在執行每一個測試案例之前,會先執行這裡面的程式碼。afterAll
─ 在執行完所有的測試案例之後,會再執行這裡面的程式碼。afterEach
─ 在執行完每一個測試案例之後,會再執行這裡面的程式碼。舉個例子,如果我們有個測試集合長這樣:
describe('Test Suite', () => {
beforeAll(() => {
console.log('beforeAll');
});
beforeEach(() => {
console.log('beforeEach');
});
it('test case - 1', () => {
console.log('test case - 1');
});
it('test case - 2', () => {
console.log('test case - 2');
});
afterEach(() => {
console.log('afterEach');
});
afterAll(() => {
console.log('afterAll');
});
});
它的執行結果會是這樣:
// beforeAll
// beforeEach
// test case - 1
// afterEach
// beforeEach
// test case - 2
// afterEach
// afterAll
從上述結果中可以看出,在一個測試集合裡會先執行的是 beforeAll
裡的程式,接著會是 beforeEach
,然後才會是測試案例;而在測試案例之後,則會先執行 afterEach
才會輪到下一個測試案例之前的 beforeEach
,再接著下一個測試案例,之後一樣會是那個測試案例之後的 afterEach
。直到最後沒有測試案例時,就執行 afterAll
裡面的程式,結束這個測試集合。
有比較理解了嗎?如果有的話,我們來試試比較複雜一點的巢狀結構:
describe('Test Suite - 1', () => {
beforeAll(() => {
console.log('beforeAll - 1');
});
beforeEach(() => {
console.log('beforeEach - 1');
});
it('test case - 1', () => {
console.log('test case - 1');
});
it('test case - 2', () => {
console.log('test case - 2');
});
describe('Test Suite - 2', () => {
beforeAll(() => {
console.log('beforeAll - 2');
});
beforeEach(() => {
console.log('beforeEach - 2');
});
it('test case - 3', () => {
console.log('test case - 3');
});
it('test case - 4', () => {
console.log('test case - 4');
});
afterEach(() => {
console.log('afterEach - 2');
});
afterAll(() => {
console.log('afterAll - 2');
});
});
afterEach(() => {
console.log('afterEach - 1');
});
afterAll(() => {
console.log('afterAll - 1');
});
});
它的執行結果會是這樣:
// beforeAll - 1
// beforeEach - 1
// test case - 1
// afterEach - 1
// beforeEach - 1
// test case - 2
// afterEach - 1
// beforeAll - 2
// beforeEach - 1
// beforeEach - 2
// test case - 3
// afterEach - 2
// afterEach - 1
// beforeEach - 1
// beforeEach - 2
// test case - 4
// afterEach - 2
// afterEach - 1
// afterAll - 2
// afterAll - 1
為讓大家比較好閱讀,我將每個測試案例稍微隔開方便大家觀察其中規律。
雖然這個例子比較複雜,但邏輯上來說跟上一個例子一樣:在開始測試某測試集合裡面的測試案例之前,會先執行該測試集合的 beforeAll
,接著是每一個測試案例的 beforeEach
,然後執行測試案例,執行完測試案例後就是 afterEach
。
比較特別需要注意的就是當要開始執行 test case - 3
之前,會先執行的是 Test Suite - 2
的 beforeAll
。原因就像上面提過的:「在開始測試某測試集合裡面的測試案例之前,會先執行該測試集合的 beforeAll
」, test case - 3
是 Test Suite - 2
裡面的測試案例,所以在開始測試 test case - 3
之前,自然會先執行該測試集合裡的 beforeAll
,接著是父層測試集合裡的 beforeEach
,才會輪到 Test Suite - 2
裡面的 beforeEach
。
這個概念在大多數的前端測試框架裡是差不多的,學一次基本適用在大多數的測試框架裡, CP 值非常之高。
雖然上述的測試執行過程看似有序,但實際上我們不能依賴這種有序,原因跟如何撰寫出優秀的測試有關,不過相信今天的內容應該已經夠燒腦了,所以明天再跟大家分享如何撰寫出優秀的測試吧!
今天的文章內容主要是要讓大家在開始撰寫測試之前,先對 Angular 的測試框架、測試檔案的內容結構有個初步的理解,如此一來有兩個好處:
此外,今天的重點主要是以下三點:
尤其是關於作用域(Scoping) 的部份,這在後續撰寫測試時,會非常常使用,所以如果有任何的問題或是回饋,請務必留言給我讓我知道噢!
寫得好清楚!
不確定 Karma 跟 Jest 的行為是不是一樣,不過這裡分享個 Jest 的小坑 XD
Hi TD,
WOW ,非常感謝你的分享,我自己在公司的專案就是用 Jest ,雖然也有遇到一些奇怪的問題,倒是沒注意到有這個問題,又學到了一件事情了!