# Karma 測試框架
<img src="https://i.imgur.com/EFkD2Qo.png">
-
講者:Chris 江江
---
## Angular 測試三劍客
* Jasmine: 提供撰寫測試所需的工具, 搭配測試運行框架在瀏覽器上執行測試。
* Karma : 測試運行框架,為開發中執行 unit test 的好選擇。(紅燈、綠燈、重構)
* Protractor: End to End Test專用。模擬使用者的操作,來判斷程式在瀏覽器上正常與否。
----
## Jasmine
#### 簡介
* 專門用來撰寫 Javascript 測試的框架
* 完全不依賴於其他的 Javascript 框架
* 語法輕巧且明確,撰寫容易
#### 測試範例撰寫重點
* describe: 描述整份測試之名稱
* beforeEach: 執行每個spec前要先執行的部分
* afterEach: 執行每個spec後要再執行的部分
* it: 即 spec, 測試案例
* expect: 期望結果
```javascript
describe('單元測試一',()=>{
beforeEach(()=>{ ... });
it('案例一',()=>{ ... });
it('案例二',()=>{ ... });
afterEach(()=>{ ... });
});
```
---
## Karma 簡介
Karma 是個既簡單又快速的測試框架, 旨在幫助開發人員能夠迅速的進行自動化單元測試。
#### 優點
1. 執行速度快
2. 可在真實環境中執行,且跨平台
3. 擴充性高
4. 可遠端控制
5. 支援CI
#### 缺點
1. 無 no serve 指令
2. (待補充)
---
## Karma 環境設定
### karma.conf.js
```javascript
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
file:[ 'src/app/**/**.spec.ts'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
angularCli: {
environment: 'dev'
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};
```
----
### 重點參數說明
* frameworks: 所使用的框架
* files: 要測試的目錄
* plugins: 依賴的第三方套件
* reporters:
1. progress 紀錄著執行或略過幾項測試以及測試總數
2. kjhtml 表示動態生成karma-jasmine-html-reporter
* browser: 想測試的瀏覽器, 需搭配plugins
* coverageIstanbulReporter: 覆蓋率測試報告
* autoWatch: 自動監測檔案是否變更
* singleRun: 預設為false, 若為true則會在測試完成時關閉瀏覽器
---
## 單元測試
#### TestBed
1. TestBed在Angular test 當中扮演著最基礎也是重要的角色。
2. 透過configureTestingModule方法,即可建立測試環境。
3. 當環境設置完成後,compileComponents 會以非同步方式進行編譯。
注意,編譯後config即不可再更動!
```javascript
TestBed.configureTestingModule({
declarations: [
AppComponent
]
});
```
將TestBed設定放到beforeEach內,
即可確保每次測試執行前環境都回到最初預設的狀態
```javascript
BeforeEach(()=>{
TestBed.configureTestingModule({
declarations: [
AppComponent
]
});
});
```
#### async
當我們要測試AppComponent時,
Angular會根據Component是否採用templateUrl與styleUrls來決定要不要發XHR。若有,則需採用async的方式來進行編譯。
```javascript
BeforeEach(async(()=>{
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
```
---
## 單元測試 - Html
* 創建測試用component,返回ComponentFixture,其中包含了可以操作DOM元素的DebugElement。
* DebugElement其實就是元件的實例,而nativeElement則是針對Dom元素操作用。可以看下方例子:
```
fixture.componentInstance == fixture.debugElement.componentInstance;
fixture.nativeElement == fixture.debugElement.nativeElement;
```
* 測試範例如下:
```javascript
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelectorAll('h1')[0].textContent)
.toContain('Chris\' Book Store');
}));
```
---
## 單元測試 - Component
* 若constructor 內有注入service,
則測試模組環境的providers就必須要有該service
```javascript
TestBed.configureTestingModule({
declarations: [ NovelComponent ],
providers: [{ provide: ExampleService, useValue: fakeService }]
})
```
* 需自行建立假的service, 取代真實的request
```javascript
const fakeService = {
getNovel: () => {
return Observable.of({
"returnCode": 200,
"data": [
{
"bookname": "Titanic",
"price": 900
},
{
"bookname": "Once",
"price": 550
},
{
"bookname": "Harry Potter",
"price": 1380
}
]
})
}
};
```
* 若template中含有 router-outlet tag,
則測試模組環境需匯入 RouterTestingModule
```javascript
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [
AppComponent
]
}).compileComponents();
```
---
## 單元測試 - Service
* 測試 Service前, 需import HttpClientModule到 TestBed config中
* 透過 Jasmine的createSpy來幫我們模擬service method
```javascript
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [ExampleService]
});
```
* 可以依照測試情境來決定, 什麼時候要注入service
```javascript
//方法1 注入service同時也catch它
beforeEach(inject([ExampleService], (eService) => {
fakeExampleService = eService;
}));
//方法2 測試時再注入service
it('should get the right response, too', inject([ExampleService], (service: ExampleService) => {
let fakeResponse = null;
service.getNovel().subscribe(res=>{
fakeResponse = res.data;
expect(fakeResponse[0].bookname).toBe('Titanic');
});
}));
```
---
## 單元測試 - Router
* 測試Router前, 需import fakeAsync、RouterTestingModule、Router與Location
```javascript
import { TestBed, fakeAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
```
* 接著進行TestBed的環境建置:將自訂的router隨著RouterTestingModule一起import到config。配置如下:
```javascript
const routes = [
{
path: '', component: null,
children: [
{ path: '', redirectTo: 'novel', pathMatch: 'full' },
{ path: 'novel', component: NovelComponent },
{ path: 'comic', component: ComicComponent },
{ path: 'magazine', component: MagazineComponent },
{ path: '**', redirectTo: 'novel' }
]
}
];
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(routes)],
declarations: [
NovelComponent,
ComicComponent,
MagazineComponent,
AppComponent
]
});
//取得注入的Router與Location
router = TestBed.get(Router);
location = TestBed.get(Location);
fixture = TestBed.createComponent(AppComponent);
router.initialNavigation();
});
```
* 利用router.navigate來測試
* 因返回promise、故可直接用then來進行後續操作(與官方的tick有些出入)
```javascript
it('should go to Comic book page', fakeAsync(() => {
router.navigate(['comic']).then(()=>{
expect(location.path()).toBe('/comic');
});
}));
```
---
## Debug Mode
1. 點擊右上角 Debug
2. 跳出新分頁後, 開啟Chrome DevTool
3. 在 Sources 標籤開啟檔案
4. 下中斷點開始除錯
---
## Coverage Report
輸入以下指令
```
ng test --code-coverage
```
Angular將會在專案目錄下生成 coverage資料夾
----
# Thanks!