# Karma & Jasmine ## 簡介 ### Karma 自動化的測試流程工具,在我們撰寫完測試文件後,下 `ng test` 指令,Karma 會找到我們的測試文件和相對應的檔案,執行 Jasmine 測試,自動開啟瀏覽器顯示測試結果,並監聽我們的修改重新執行測試。 > 預設測試結果會用 chrome 開啟 `http://localhost:9876/` 顯示,可以在 Karma 的設定檔 `src/karma.conf.js` 中修改。 > ### Jasmine Jasmine 測試框架是 Angular 內建測試的主要工具。Jasmine 不相依於其他庫或是框架,所以可以在任何框架下使用,或是為 Vanilla JS 做測試。 一個簡單的 test suite 架構如下: ```javascript= describe("A spec", function() { let foo; // 每個測試案例開始前會做的事 beforeEach(function() { foo = 0; foo += 1; }); // 每個測試案例結束後會做的事 afterEach(function() { foo = 0; }); it("is just a function, so it can contain any code", function() { // 對預期的斷言: // expect( <實際值> ).toEqual( <期望值> ); expect(foo).toEqual(1); }); it("can have more than one expectation", function() { expect(foo).toEqual(1); expect(true).toEqual(true); }); describe("nested inside a second describe", function() { let bar; beforeEach(function() { bar = 1; }); it("can reference both scopes as needed", function() { expect(foo).toEqual(bar); }); }); }); ``` 測試成功會出現如下畫面: ![](https://i.imgur.com/qDXJFti.png) beforeEach / afterEach 用來在每個 spec(it)執行前後,做環境建置和清除的工作,expect 和後面串接的 .toEqual 做實際值和期望值的比對,當 spec 內所有的 expect 都為真,則該筆 spec 測試通過。 ## Jasmine 常用函式 ### describe 用來將相關連的測試單元(spec,即上例 `describe` 中的 `it`)集中在一起,並用第一個字串參數加以描述。通常在一個檔案中會有一個最頂層的 describe,但 describe 中可以包含很多筆 describe 和 spec。 :::info 如果想要 pending 某一筆 describe,可以在前面加上 x(即 xdescribe),這樣這筆 describe 會被列在 spec lists 上,但不被執行。如果只想執行某一筆 describe 則可以在前面加上 f(即 fdescribe),則只有該筆會被執行。 ::: ### beforeEach / beaforeAll / afterEach / afterAll 在每個 spec 執行之前,都會執行一次 beforeEach。同理,afterEach 是在每個 spec 執行完成之後,會被執行一次;而 beforeAll 及 afterAll 則各會在所在的 describe 內所有的 spec 執行之前和之後執行一次。常用來為每個 spec 做環境初始化和清除。 ### it 就是所謂的 spec,即實際的測試案例。第一個字串參數用來描述這個測試,在描述撰寫得完善的測試中,describe 與 spec 的敘述應該會串接成完整的句子。 ![](https://www.ibm.com/developerworks/cn/web/1404_changwz_jasmine/image009.jpg =360x) :::info pending spec 的方式有兩種,第一種方式如同 describe,在 it 前面加上 x(即 xit),第二種可以在 spec 中加入 pending 函式。而如果只想執行某一筆 spec,則可以在前面加上 f(即 fit)。 ::: ### expect 在 spec 中的 expect 即為一個斷言,也就是描述了期望的結果。當 `expect(value).toBe(0);` 中, value 的值為 `0` 時,表示這個斷言為真。當一個 spec 中的所有 expect 都為真時,才代表通過這條 spec 測試。expect 後面串接的 toBe 為 matcher。 ### matcher matcher 用來在實際值與期望值之間作比較,並通知 Jasmine 這個 expect 是否為真。 Matcher | 說明 :--------------------|:---- toBe | 效果類似 ===,用以比較數值是否相等。 toEqual | 用來比較 object 或是 array 等 call by reference 的數值的內容值是否相等。(用 toBe 或 toEqual 對於 primitive value 效果是一樣的) toContain | 檢驗陣列或字串中是否包含某元素或子字串 toBeTruthy | 是否可以轉換為 `true` toMatch | 是否符合正規表達式 toBeDefined | 變數是否被定義 toBeNull | 是否為 null toHaveBeenCalled | 檢查被監聽的函式是否被呼叫過 [matcher 官方完整列表](https://jasmine.github.io/api/edge/matchers.html) 另外,如果在 matcher 前面串接 `.not`,則代表相反結果。例: ```javascript= let foot = 2; ... expect(foo).not.toBe(1); //true ``` ### Spy Spy 是 Jasmine 提供用來植入測試主體的 stub,用來模擬函式執行。常用在模擬測試主體的依賴,將測試主體與依賴隔離開來,以避免太多變因干擾了測試結果。 #### spyOn 用來在已經存在的物件的 method 中植入 spy。(要植入在 property 請改用 `spyOnProperty`) ```javascript= // app.component.ts export class AppComponent implements OnInit { click() { ... } ngOnInit(): void { this.click(); } } describe('AppComponent', () => { it('should be call method click', () => { const comp = new AppComponent(); spyOn( comp, 'click' ); comp.ngOnInit(); expect(comp.click).toHaveBeenCalled(); }) }) ``` 上例中,在 AppComponent 的實體 comp 的 click() 已被 spy 偽裝,可以用 toHaveBeenCalled 來測試是否被正確的綁定。 還可以使用 `callFake` 來模擬函式的執行、`returnValue` 來模擬回傳值,或是使用 `callThrough` 來測試實際的函式執行結果: ```javascript= export class AppComponent implements OnInit { data; click() { return 'Hello' } ngOnInit(): void { this.data = this.click(); } } describe('AppComponent', () => { const comp = new AppComponent(); it('test click with callFake', () => { // callFake 傳入 callback funciton 執行 spyOn( comp, 'click' ).and.callFake( () => 'Bar' ); comp.ngOnInit(); expect(comp.data).toBe('Bar'); }); it('test click with returnValue', () => { // returnValue 模擬 spy 回傳數值 spyOn( comp, 'click' ).and.returnValue('Foo'); comp.ngOnInit(); expect(comp.data).toBe('Foo'); }); it('test click with callThrough', () => { // callThrough 直接執行被 spy 的函式 // 但仍保有 spy 可被追蹤的特性 spyOn( comp, 'click' ).and.callThrough(); comp.ngOnInit(); expect(comp.data).toBe('Hello'); }); }); ``` #### jasmine.createSpyObj 建立一個 帶有 spy method 的 object,常用來製作相依 Service 的 stub: ```javascript= //app.component.ts export class AppComponent implements OnInit { data = 'default'; constructor( private stateService: StateService ) { } ngOnInit(): void { this.data = this.stateService.getState(); } } //app.component.spec.ts describe('AppComponent', () => { const testData = 'Hello World'; const mockState = jasmine.createSpyObj('StateService', ['getState']); let comp; beforeEach( () => { TestBed.configureTestingModule({ providers: [ AppComponent, { provide: StateService, useValue: mockState } ] }); comp = TestBed.get(AppComponent); }); it('test StateService binding', () => { mockState.getState.and.returnValue(testData); comp.ngOnInit(); expect(comp.data).toBe(testData); }); }); ``` 上例用 jasmine.createSpyObj 來製作帶 getState method 的 mockUserService 實例,並使用 TestBed 來注入元件中。 在 spec 中,設定了 getState 的回傳值,並驗證了在 ngOnInit 執行後綁定結果是否正確。