当前位置: 首页 > 資訊 >

Angular 深入淺出三十天:表單與測試 Day04 - 開始撰寫測試之前必須要知道的二三事

Day4

在開始撰寫測試之前,先帶大家來了解一下 Angular 預設使用的測試框架 ─ Karma

Karma 的 logo

Karma 的原名是 Testacular , Google 在 2012 年的時候將其開源, 2013 年時將其改名為 Karma ,它是基於 JasmineSelenium 所開發出來的 JavaScript 測試執行過程管理工具(Test Runner)。

一般我們會使用它來撰寫單元測試整合測試,測試的檔案名稱通常會命名為 xxx.spec.ts ,而只要是使用 Angular CLI 所建立的檔案,在預設的情況下都會連帶產生該檔案,像是: xxx.component.spec.tsxxx.service.spec.ts

當我們想要執行測試程式時,只要使用指令 npm test or yarn test or ng test ,就可以看到它的執行結果:

Karma's log

Karma's view

當 Karma 執行起來後,只要我們不停掉它的 server 且不關掉它的視窗,只要我們有修改我們的測試並存檔後,它就會偵測到我們的變動並再重新跑一次測試,是個很方便且強大的功能。

關於執行測試時的更多參數,請參考Angular 官方 API 文件

想了解更多的話,可參考網路文章:JavaScript 測試工具之 Karma-Jasmine 的安裝和使用詳解Karma 官方文件

測試的檔案內容

上述提到,在 Angular 裡的測試檔案一般我們會將其命名為 xxx.spec.ts ,而檔案內容大致上會長這樣:

Testing Sample

或是這樣:

Testing Sample

從中我們可以發現,它是一種巢狀式的結構,外層會是一個名字叫 describe 的函式,內層則有許多名為 it 的函式,這些函式各是什麼意思呢?

it

it 指的是 測試案例(Test case),通常會在 describe 函式的裡面,使用方式如下所示:

it('說明文字', () => {
  // test content
});

第一個參數是該測試案例的說明文字,讓我們在閱讀時可以很清楚、直接地知道這個測試案例會有什麼結果,通常建議以 should 做開頭,整體閱讀起來較為順暢,例如:

it('should be created', () => {
  // test content
});

或者像是:

it('should have as title "Angular"', () => {
  // test content
});

第二個參數是一個函式,裡面就是該測試案例所要執行的程式碼,也就是我們實際上要測試的內容。

describe

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)

作用域(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 - 2beforeAll 。原因就像上面提過的:「在開始測試某測試集合裡面的測試案例之前,會先執行該測試集合的 beforeAll 」, test case - 3Test Suite - 2 裡面的測試案例,所以在開始測試 test case - 3 之前,自然會先執行該測試集合裡的 beforeAll ,接著是父層測試集合裡的 beforeEach ,才會輪到 Test Suite - 2 裡面的 beforeEach

這個概念在大多數的前端測試框架裡是差不多的,學一次基本適用在大多數的測試框架裡, CP 值非常之高。

雖然上述的測試執行過程看似有序,但實際上我們不能依賴這種有序,原因跟如何撰寫出優秀的測試有關,不過相信今天的內容應該已經夠燒腦了,所以明天再跟大家分享如何撰寫出優秀的測試吧!

本日小結

今天的文章內容主要是要讓大家在開始撰寫測試之前,先對 Angular 的測試框架、測試檔案的內容結構有個初步的理解,如此一來有兩個好處:

  1. 後續不用再解釋,文章內容可以比較精簡
  2. 有需要時可以回來複習

此外,今天的重點主要是以下三點:

  1. 認識 Angular 預設所使用的測試框架。
  2. 了解測試檔案的內容結構。
  3. 理解作用域(Scoping) 的邏輯。

尤其是關於作用域(Scoping) 的部份,這在後續撰寫測試時,會非常常使用,所以如果有任何的問題或是回饋,請務必留言給我讓我知道噢!