---
# System prepended metadata

title: Karma & Jasmine

---

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