# 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);
});
});
});
```
測試成功會出現如下畫面:

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 的敘述應該會串接成完整的句子。

:::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 執行後綁定結果是否正確。