Angular2 之 單元測試

單元測試需要掌握的知識點

  • karma.conf.js的配置 具體瞭解到每一項的意義,這樣才能真正的瞭解這個配置是如何配置的,甚至纔可以做到自己的配置。
  • 組件的測試
  • 單獨的service測試

Angular的測試工具

Angular的測試工具類包含了TestBed類和一些輔助函數方法,當時這不是唯一的,你可以不依賴Angular 的DI(依賴注入)系統,自己new出來測試類的實例。

孤立的單元測試

describe('Service: base-data-remote', () => {
  let service = new BaseDataRemoteService();

  it('should be created',() => {
    expect(service).toBeTruthy();
  });
});

利用Angular測試工具進行測試知識點總結

測試工具包含了TestBed類和@angular/core/testing中的一些方法。

  • 在每個spec之前 ,TestBed將自己重設爲初始狀態。

測試組件

import { Component }   from '@angular/core';

@Component({
  selector: 'app-banner',
  template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
  title = 'Test Tour of Heroes';
}
let comp:    BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de:      DebugElement;
let el:      HTMLElement;

describe('BannerComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ BannerComponent ], // declare the test component
    });

    fixture = TestBed.createComponent(BannerComponent);

    comp = fixture.componentInstance; // BannerComponent test instance

    // query for the title <h1> by CSS element selector
    de = fixture.debugElement.query(By.css('h1'));
    el = de.nativeElement;

  });
});
  • 組件測試
    • TestBed.createComponent創建BannerComponent組件的實例,可以用來測試和返回fixture
    • TestBed.createComponent關閉當前TestBed實例,讓它不能再被配置。
    • query方法接受predicate函數,並搜索fixture的整個DOM樹,試圖尋找第一個滿足predicate函數的元素。
    • queryAll方法返回一列數組,包含所有DebugElement中滿足predicate的元素。
    • By類是Angular測試工具之一,它生成有用的predicate。 它的By.css靜態方法產生標準CSS選擇器 predicate,與JQuery選擇器相同的方式過濾。
    • detectChanges:在測試中的Angular變化檢測。
      每個測試程序都通過調用fixture.detectChanges()
      來通知Angular執行變化檢測。

測試有依賴的組件,這個依賴的測試

這個依賴的模擬方式有兩種:僞造服務實例(提供服務複製品)、刺探真實服務。這兩種方式都不錯,只需要挑選一種最適合你當前測試文件的測試方式來做最好。

僞造服務實例

被測試的組件不一定要注入真正的服務。實際上,服務的複製品(stubs, fakes, spies或者mocks)通常會更加合適。 spec的主要目的是測試組件,而不是服務。真實的服務可能自身有問題。

這個測試套件提供了最小化的UserServiceStub類,用來滿足組件和它的測試的需求。

userServiceStub = {
  isLoggedIn: true,
  user: { name: 'Test User'}
};

獲取注入的服務

測試程序需要訪問被注入到組件中的UserService(stub類)。

Angular的注入系統是層次化的。 可以有很多層注入器,從根TestBed創建的注入器下來貫穿整個組件樹。

最安全並總是有效的獲取注入服務的方法,是從被測試的組件的注入器獲取。 組件注入器是fixture的DebugElement的屬性。

出人意料的是,請不要引用測試代碼裏提供給測試模塊的userServiceStub對象。它是行不通的! 被注入組件的userService實例是徹底不一樣的對象,是提供的userServiceStub
的克隆。

  • TestBed.get方法從根注入器中獲取服務。
    例如:
    dataService = testBed.get(DataService);

測試代碼

  beforeEach(() => {
    // stub UserService for test purposes
    userServiceStub = {
      isLoggedIn: true,
      user: { name: 'Test User'}
    };

    TestBed.configureTestingModule({
       declarations: [ WelcomeComponent ],
       // 重點
       providers:    [ {provide: UserService, useValue: userServiceStub } ] 
    });

    fixture = TestBed.createComponent(WelcomeComponent);
    comp    = fixture.componentInstance;

    // UserService from the root injector
    // 重點
    userService = TestBed.get(UserService);

    //  get the "welcome" element by CSS selector (e.g., by class name)
    de = fixture.debugElement.query(By.css('.welcome'));
    el = de.nativeElement;
  });

刺探(Spy)真實服務

注入了真是的服務,並使用Jasmine的spy替換關鍵的getXxxx方法。

spy = spyOn(remoteService, 'getTodos').and.returnValues([Promise.resolve(datas), Promise.resolve(datas2)]);

Spy的設計是,所有調用getTodos的方法都會受到立刻解析的承諾,得到一條預設的名言。

it方法中的幾個函數

寫單元測試時,it裏經常會有幾個常見的方法,async(),fakeAsync(),tick(),jasmine.done()方法等。
這幾個方法,都幫助我們簡化了異步測試程序的代碼。但是需要正確的使用這幾個方法。

組件

@Component({
  selector: 'twain-quote',
  template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent  implements OnInit {
  intervalId: number;
  quote = '...';
  constructor(private twainService: TwainService) { }

  ngOnInit(): void {
    this.twainService.getQuote().then(quote => this.quote = quote);
  }
}
  • async
    • Angular TestBed的一部分。通過將測試代碼放到特殊的異步測試區域來運行,async函數簡化了異步測試程序的代碼。
    • 接受無參數的函數方法,返回無參數的函數方法,變成Jasmine的it函數的參數。
    • 它的參數看起來和普通的it參數主體一樣。 沒有任何地方顯示異步特徵。 比如,它不返回承諾,並且沒有done方法可調用,因爲它是標準的Jasmine異步測試程序。

使用例子:

it('should show quote after getQuote promise (async)', async(() => {
  fixture.detectChanges();

  fixture.whenStable().then(() => { // wait for async getQuote
    fixture.detectChanges();        // update view with quote
    expect(el.textContent).toBe(testQuote);
  });
}));
  • 簡單介紹一下whenStable()方法
    • 測試程序必須等待getQuote在JavaScript引擎的下一回閤中被解析。
    • ComponentFixture.whenStable方法返回它自己的承諾,它getQuote
      承諾完成時被解析。實際上,“stable”的意思是當所有待處理異步行爲完成時的狀態,在“stable”後whenStable承諾被解析。
    • 然後測試程序繼續運行,並開始另一輪的變化檢測(fixture.detectChanges
      ),通知Angular使用名言來更新DOM。 getQuote
      輔助方法提取出顯示元素文本,然後expect語句確認這個文本與預備的名言相符。
  • fakeAsync
    • fakeAsync是另一種Angular測試工具。
    • async一樣,它也接受無參數函數並返回一個函數,變成Jasmine的it
      函數的參數。
    • fakeAsync函數通過在特殊的fakeAsync測試區域運行測試程序,讓測試代碼更加簡單直觀。
    • 對於async來說,fakeAsync最重要的好處是測試程序看起來像同步的。裏面沒有任何承諾。 沒有then(…)鏈來打斷控制流。
  • tick
    tick函數是Angular測試工具之一,是fakeAsync的同伴。 它只能在fakeAsync的主體中被調用。
    • 調用tick()模擬時間的推移,直到全部待處理的異步任務都已完成,在這個測試案例中,包含getQuote承諾的解析。

使用例子

it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
  fixture.detectChanges();
  tick();                  // wait for async getQuote
  fixture.detectChanges(); // update view with quote
  expect(el.textContent).toBe(testQuote);
}));
  • jasmine.done
    雖然async和fakeAsync函數大大簡化了異步測試,但是你仍然可以使用傳統的Jasmine異步測試技術。
    你仍然可以將接受 done回調的函數傳給it。 但是,你必須鏈接承諾、處理錯誤,並在適當的時候調用done。
    使用例子
it('should show quote after getQuote promise (done)', done => {
  fixture.detectChanges();

  // get the spy promise and wait for it to resolve
  spy.calls.mostRecent().returnValue.then(() => {
    fixture.detectChanges(); // update view with quote
    expect(el.textContent).toBe(testQuote);
    done();
  });
});

以上這三個測試例子是等價的,也就是說,你可以隨你喜好選擇你喜歡的測試方式來進行單元測試的編寫。

測試有外部模板的組件

使用例子

// async beforeEach
beforeEach( async(() => {
  TestBed.configureTestingModule({
    declarations: [ DashboardHeroComponent ],
  })
  .compileComponents(); // compile template and css
}));

beforeEach裏的async函數

注意beforeEach裏面對async的調用,因爲異步方法TestBed.compileComponents而變得必要。

compileComponents

  • 在本例中,TestBed.compileComponents編譯了組件,那就是DashbaordComponent。 它是這個測試模塊唯一的聲明組件。
  • 本章後面的測試程序有更多聲明組件,它們中間的一些導入應用模塊,這些模塊有更多的聲明組件。 一部分或者全部組件可能有外部模板和CSS文件。 TestBed.compileComponents一次性異步編譯所有組件。
  • compileComponents方法返回承諾,可以用來在它完成時候,執行更多額外任務。

測試帶有inputs和outputs的組件

測試前期代碼

  // async beforeEach
  beforeEach( async(() => {
    TestBed.configureTestingModule({
      declarations: [ DashboardHeroComponent ],
    })
    .compileComponents(); // compile template and css
  }));

  // synchronous beforeEach
  beforeEach(() => {
    fixture = TestBed.createComponent(DashboardHeroComponent);
    comp    = fixture.componentInstance;
    heroEl  = fixture.debugElement.query(By.css('.hero')); // find hero element

    // pretend that it was wired to something that supplied a hero
    expectedHero = new Hero(42, 'Test Name');
    comp.hero = expectedHero;
    fixture.detectChanges(); // trigger initial data binding
  });

屬性

測試代碼是將模擬英雄(expectedHero)賦值給組件的hero屬性的。

// pretend that it was wired to something that supplied a hero 
expectedHero = new Hero(42, 'Test Name'); 
comp.hero = expectedHero;

點擊事件

  it('should raise selected event when clicked', () => {
    let selectedHero: Hero;
    comp.selected.subscribe((hero: Hero) => selectedHero = hero);

    heroEl.triggerEventHandler('click', null);
    expect(selectedHero).toBe(expectedHero);
  });

這個組件公開EventEmitter屬性。測試程序像宿主組件那樣來描述它。

heroEl是個DebugElement,它代表了英雄所在的

。 測試程序用”click”事件名字來調用triggerEventHandler。 調用DashboardHeroComponent.click()時,”click”事件綁定作出響應。

如果組件想期待的那樣工作,click()通知組件的selected屬性發出hero對象,測試程序通過訂閱selected事件而檢測到這個值,所以測試應該成功。

triggerEventHandler

Angular的DebugElement.triggerEventHandler可以用事件的名字觸發任何數據綁定事件。 第二個參數是傳遞給事件處理器的事件對象。


自己遇到的坑兒

下面都是自己在實際的編寫單元測試時,真實遇到的問題,自己真的是在這上面花費了很多時間啊!!!爲什麼沒有說花冤枉時間呢?就是因爲是自己對單元測試還沒喲掌握,所以出了錯,不要緊,重要的是以後不能再犯!

service的注入

剛剛接觸angular2吧,對很多service的寫法不是很瞭解,以至於真的是白白浪費了很多時間,尤其是在這個service的模擬上。可能聰明如你,不會犯我這樣簡單卻又致命的錯誤吧,只希望,以後的賀賀也可以不再犯這樣的錯!


寫在後面

GitHub上集大家之力搞了一個前端面試題的項目,裏面都是大家面試時所遇到的題以及一些學習資料,有興趣的話可以關注一下。如果你也有興趣加入我們的話,請在項目中留言。項目同時也可以在gitbook上查看。

InterviewLibrary-GitHub
InterviewLibrary-gitbook

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章