# Angular Unit Testing
## 測試程式的架構邏輯
下方是我們要測試的function
```javascript=
function helloWorld() {
return 'Hello World';
}
```
接著是我們的測試程式碼:
```javascript=
describe('Hello World', () => {
it('show hello world', () => {
expect(helloWorld()).toEqual('Hello World');
});
});
```
1.從測試程式碼中可以看到 describe(string, function); 這個開頭。 後方的 function 這是Test Suite 裡面會有許多我們的關於 string (Hello World)的獨立單元測試,所以可說function是單元測試的集合。
2.it(string, function) 這個定義了我們每一個獨立的單元測試,這會包含一個或是多個的Expectation(期望)。
3.expect(actual) 這個描述我們稱之為 Expectation (期望)後方會接著呼應的結果(Matcher)來描述我們這個測試『期望的結果』是如何,通常這個matcher(expected)的描述就是我們說的呼應結果(Matcher)其值為boolean。
4.期待值(expected)與實際值(actual value)被丟到expect這個function內做比較,如果出來的值為false就代表這個測試失敗。
所以上述的那段測試碼就是說:這一段是做 Hello World的相關測試,然後 show hello world 這個單元測試就是~期望(expect)helloWorld()這個function 會等於(toEqual) 'Hello World'這個字串。
有時候在測試某些功能時我們會預先建立一些物件、服務之類的或是在單元測試結束後要求清除一些檔案或是變數因此Jasmine提供了下列幾個特殊Function供我們測試時做『預處理』或是『後處理』。
### beforeAll
這個function只會在describe()內的所有測試之前被呼叫一次。
### afterAll
這個function會在所有的 describe() 都做完之後被呼叫一次。
### beforeEach
這個function會在每一個 it function執行之前被執行。
### afterEach
這個function會在每一個單元測試後被呼叫。
#### 2019-01-07 補充:
忽略某些測試的方法
如果想要 Jasmine 在測試的時候忽略某些已經寫好的測試碼時,除了註解掉之外另外一種就是在 describe() 或是 it() 的前面加上 'x' 也就是變成 xdescribe() 或是 xit() 。這樣 Jasmine 一樣會列出這些測試的名稱但是不會真的進行測試,而且結果的圈圈會打上灰色的圓圈。
#### 專注某些測試
同樣專注某些測試的方法就是在 describe() 與 it() 前面加上 'f' ,這樣在測試結果的 report 中會把這些需要專注的測試清單列在最前面,連同失敗或是成功的綠色圈圈也會被列在最前方。
## 測試 service 元件
Service 元件通常在前端比較常見的功能就是向後端要求資料,所以 API 的功能通常都會寫在 service 元件裡面。這邊我們會建立一個測試 service 元件的專案。一樣先提供我的開發環境。
## TestBed :
可以說是 Angular 測試工具最重要的東西,它會建立一個動態的、虛擬的、可初始化的 Angular @NgModule 來當作我們測試的平台。
## TestBed.configureTestingModule():
這個 method 會設定一個擁有@NgModule 的物件。
所以我們可以看出程式就是在'DemoService' 內 it 這個測試之前,我們會產生一個@NgModule的物件。這個物件其實就是我們拿來模擬 app.module.ts 的功用。但是由於我們是作單元測試因此不像我們的app.module.ts 一樣 declaretions 跟 import 一堆東西。
# Angular Testing - Angular Taiwan
## 為什麼要寫測試
1.程式發生錯誤,怎麼模擬出當時的狀況及 Debug?
2.改了某個 function,會不會導致別的程式掛掉?
3.到底測過了哪些功能,有沒有漏掉的沒測?
## 目的
1.能快速提供反饋。
2.提升程式碼品質。
3.節省測試與除錯時間。
4.整體專案時程縮短。
## 種類
1.單元測試
2.整合測試
3.驗收測試
### 單元測試
1.用來模擬外部如何使用廁是目標物件,驗證其行為是否符合預期。
2.從程式最小的功能開始。
3.單元指的是一個類別或模組。
### 整合測試
將兩個以上的類別做整合,測試它們之間的運作關係是不是正確的。
### 驗收測試
1.系統行為與功能面的規範。
2.用來說明某一個 user story。
3.從使用者的角度來檢視 (是否符合使用者的期望)。
## 心法
1.在編寫某個功能的代碼之前先編寫測試代碼,然後只編寫使測試通過的功能代碼。
2.所有的實現都是測試「逼」出來的,所有的實現代碼都是為了讓測試通過而編寫的。
3.先以調用方的角度來調用代碼,並且從調用方的角度說出所期望的結果。
4.編寫測試時就僅僅關注測試,不想去如何實現。
```javascript=
//先寫好測試範例
it('應該說 Him Leo', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.hi('Leo').toEqual('Hi, Leo'));
});
//從測試知道應該是寫一個 hi function 來帶入字串參數
hi(name: string):string {
return `Hi, ${name}`;
}
```
5.絕不跳過重構。
6.出錯後放慢腳步。
## 覆蓋率
1.測試程式碼的涵蓋範圍。
2.請把覆蓋率的數字當作一個健康指標,用來檢查:
- 重要的 Scenario 有沒有涵蓋多一點情境?
- 現在有哪些程式碼沒有被測試過?
- 發生測試失敗時,失敗的原因點是否有被測試案例涵蓋到?
## TDD vs ATDD
- TDD:測試驅動開發(Test-Driven Development)
- ATDD:驗收測試驅動開發(Acceptance Test Driven Development)
### 軟體開發過程中最常見的問題
- 用戶想要的功能沒有開發。
- 開發的功能並非用戶想要。
- 用戶和開發人員所說語言不同。

###
```javascript=
// describe 描述測試的 component
describe('PrivateCallComponent', () => {
let component: PrivateCallComponent;
let fixture: ComponentFixture<PrivateCallComponent>;
// beforeEach 是在每一個 it 之前先執行的事情
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PrivateCallComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PrivateCallComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// beforeALL 在所有的 it 之前只執行這一次
beforeALL();
// afterEach 在每一個 it 之後做的事情
afterEach();
// it 是測試案例
it('should create', () => {
expect(component).toBeTruthy();
expect(true).toBeTruthy();
});
// 在 function 前面加上 x 可以忽略不測試該功能,或是加上 f 只測試該功能
// 例如 xit 、 fit
// 也可以巢狀寫法,describe 裡面在有 describe, beforeEach 也能寫巢狀,只會影響同一層範圍。
describe('By Leo', () => {
it('應該說 Hi, Leo', () => {
except(app.hi('Leo')).toEqual('Hi, Leo');
});
});
});
```
# Jasmine & Protractor 簡介 - Angular Taiwan
Jasmine & Protractor 這兩個前端測式框架,可以被使用在任何的前端網站開發中,在 angular-cli 中也有被應用。
## Jasmine
一套 JS 的測試 framework ,從 2009 年開始至今,是一套非常老牌的框架。
```javascript=
// install jasmine
npm install --save-dev jasmine
// initialize Jasmine in your project
./node_modules/.bin/jasmine init
```
```javascript=
// describe 描述測試
describe('first test', () =>{
// it 是單一測試規格
it('hello world'. () => {
// expect 是預期的期望值
expect(true).toBe(true);
});
// toEqual 值必須是相等的
it('toEqual', () => {
let a = 11;
expect(a).toEqual(11); // true
expect(a).toeEqual(12); //fail
expect(a).not.toeEqual(12); //true
});
// less & greater
it('less & greater', () => {
let pi = 3.14159;
let e = 2.78;
expect(e).toBeLessThan(pi); //true
expect(e).not.toBeGreaterThan(pi); //true
});
// throw 呼叫 function時候,拋出指定的值
it('throw', () => {
var foo = function () {
throw new TypeError ('foo bar baz');
};
expect(foo).toThrow();
expect(foo).toThrowError('foo bar baz');
expect(foo).toThrowError(/bar/);
expect(foo).toThrowError(TypeError);
expect(foo).toThrowError(TypeError, 'foo bar baz');
});
});
```
### Spies
在測試中,mock 的手法經常用到(在 jasmine 中叫做 spy),尤其是要呼叫 api或是使用第三方套件時,那些 function 我們認為是正確的,所以不希望包進來測試,就做模擬的方式取代掉真實的呼叫,專注於我們自己所要測試的功能上。
```javascript=
describe('a spy', function(){
var foo, bar = null;
beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
}
};
spyOn(foo, 'setBar');
foo.setBar(123);
foo.setBar(456, 'another param');
});
it('tracks that the spy was called', function() {
expect(foo.setBar).toHaveBeenCalled();
});
it('tracks all the arguments of its calls', function() {
expect(foo.seBar).toHaveBeenCalled(123);
expect(foo.seBar).toHaveBeenCalled(456, 'another param');
});
it('stops all execution on a function', function() {
expect(bar).toBeNull();
});
});
```
```javascript=
describe('a spy', function(){
var foo, bar = null;
beforeEach(function() {
foo = {
getBar: function() {
return bar;
},
getBar1: function() {
return bar;
}
};
spyOn(foo, 'getBar').and.returnValue(789);
spyOn(foo, 'getBar1').and.callFake(() => {
return 0;
});
});
it('return value', function() {
expect(foo.getBar()).toEqual(456); //error value是789
});
it('call fake', () => {
expect(foo.getBar1()).toEqual(0);
})
});
```
## Protractor
由 Angular 官方出的一套 end-to-end(e2e)的 framework. 測試期間會開啟一個瀏覽器去執行所要測試的項目,如果是使用 angular-cli 所產生的專案。預設就已經包含,設定放在根目錄底下的 protractor.conf.js
```javascript=
//透過一個 webdriver 去取得瀏覽器的內容
npm install -g protractor
webdriver-manager update
webdriver-manager start
#http://localhost:4444/wd/hub
```
接著要設定一個 conf.js 檔,並且寫好測試檔 spec.js
```javascript=
exports.config = {
framework: 'jasmine',
seleniumAddress: 'http://localhost:4444/wd/hub',
specs:['spec.js']//測試規格檔
}
```
```javascript=
// spec.js
describe('Protractor Demo App', function() {
it('should have a title', function() {
browser.get('http://juliemr.github.io/protractor-demo/');
expect(browser.getTitle()).toEqual('Super Calculator');
});
beforeEach(() => {
browser.get('http://juliemr.github.io/protractor-demo/');
});
it('get first input', () => {
let elm = element(by.css('.input-small'));
elm.sendKeys('1');
expect(elm.getAttribute('value')).toEqual('1');
})
it('get all input',() => {
let els = element.all(by.css('.input-small')); //els 是 array
let first = els.get(0); // 取 array 中的第一個
let first1 = els.first(); // 取 array 中的第一個
first.sendKeys('1');
let sec = els.get(1);
// ley sec = els.last();
sec.sendKeys('2');
browser.sleep(1000);
})
});
```
Protractor 執行指令
```javascript=
protractor conf.js
```
# Karma 測試框架 - Angular Taiwan
## Angular 測試三劍客
- Jasmine: 提供測試撰寫所需的工具,搭配測試運行框架在瀏覽器上執行測試。
- Karma: 測試運行框架,為開發中執行 unit test 的好選擇。(紅燈、綠燈、重構)
- Protractor: End to End Test 專用。模擬使用者的操作,來判斷程式在瀏覽器上正常與否。
## Jasmine
### 簡介
- 專門用來撰寫 JavaScript 測試的框架
- 完全不一賴於其他的 JavaScript 框架
- 語法輕巧且明確,撰寫容易
## Karma
Karma是個既簡單又快速的測試框架,旨在幫助開發人員能夠迅速的進行自動化單元測試。
### 優點
- 執行速度快
- 可在真實環境中執行,且跨平台
- 擴充性高
- 可遠端控制
- 支援CI
### 缺點
- 無 no serve 指令
### Karma 環境設定
```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 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
});
};
```
### 單元測試
- TestBed 在 Angular test 當中扮演著最基礎也是重要的角色。
- 透過 configure TestingModule 方法,即可建立測試環境。
- 當環境設置完成後,complieComponents 會以非同步方式進行編譯。
```javascript=
TestBed.configureTestingModule({
declarations: [
AppComponent
]
});
```
將 TestBed 設定放到 beforeEach,即可確保每次測試執行前環境都回到最初預設的狀態
```javascript=
BeforeEach(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
]
});
});
```
當我們要測試 AppComponent 時,Angular 會根據 Component 是否採用 templateUrl 與 styleUrls 來決定要不要發 XHR。若有,則須採用 async 的方式來進行編譯。由於 Angular Cli 是使用 webpack 建置專案,webpack 已經把呼叫 template 和 style 這兩個動作先執行,因而不需要使用 Async 和 complieComponents(),但不是使用 webpack 做測試時,還是要執行上述步驟。
```javascript=
BeforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
]
}).compileComponents();
}));
```
### 單元測試 - Html
- 創建測試用 component,返回 ComponentFixture,其中包含了可以操作 DOM 元素的 DebugElement。
- DebugElement 其實就是元件的實例,而 nativeElement 則是針對 DOM 元素操作用。可以看下方例子:
```javascript=
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();//detectChanges 會在執行時,去監測 angular 目前還有什麼變動,當變動完畢,才會繼續執行步驟。
const complied = 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, useVale: 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
]
}).complieComponents();
```
單元測試 - 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: '', redirecTo: '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');
});
}));
```
#### 覆蓋率測試
```javascript=
ng test --code-coverage
```
生成一個 coverage資料夾,會產生一個 html 在資料夾內,可以開啟該檔案來查看。