---
# System prepended metadata

title: Angular Unit Testing

---

# 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）
### 軟體開發過程中最常見的問題
- 用戶想要的功能沒有開發。
- 開發的功能並非用戶想要。
- 用戶和開發人員所說語言不同。

![](https://i.imgur.com/a5iL1aW.png)

### 
```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 在資料夾內，可以開啟該檔案來查看。

 
