Angular + Rxjs 單元測試

最近進入到新的項目,前端采用Angular開發,惡補了一陣子關于Angular的知識,跟Angular相關的Rxjs和Ngrx也不能少。經過幾個小的功能模塊的開發后,也開始適應了Angular+Rxjs+Ngrx的開發模式。

但是,寫UT的時候,卻又是【一!臉!懵!逼!】

使用TestBed解決注入問題

首先面臨的問題是:Angular一般會使用依賴注入(DI)來創建服務, 當某個服務依賴另一個服務時,DI 就會找到或創建那個被依賴的服務。 如果那個被依賴的服務還有它自己的依賴,DI 也同樣會找到或創建它們。但是在編寫單元測試的時候,誰來幫忙做注入?

下面是一個例子:

// file: Master.service.ts

@Injectable()
export class MasterService {
  constructor(private subService: SubService) { }
  getValue() { return this.subService.getValue(); }
}

MasterService 依賴于SubService, 通過構造函數注入,SubService的定義如下:

// file: Sub.service.ts

@Injectable()
export class SubService {
  constructor() { }
  getValue() { return "value"; }
}

如果要為MasterService寫UT,就需要解決依賴問題。

方法一:

比較“笨”的一個解決辦法就是自己在單元測試中主動創建要注入的實例,然后作為構造函數傳遞,如下:

// file: master.service.spec.ts

describe('MasterService without Angular testing support', () => {
  let masterService: MasterService;
 
  it('#getValue should return real value from the real service', () => {
    //new一個SubService的實例,然后通過MasterService的構造函數傳入 
    masterService = new MasterService(new SubService());
    expect(masterService.getValue()).toBe('real value');
  });
 
  it('#getValue should return faked value from a fakeService', () => {
    //new一個假的SubService實例,但是它實現了Subservice的方法,然后通過MasterService的構造函數傳入
    masterService = new MasterService(new FakeSubService());
    expect(masterService.getValue()).toBe('faked service value');
  });
 
  it('#getValue should return stubbed value from a spy', () => {
    // 通過jasmin創建一個spy對象,把spy對象通過MasterService的構造函數傳入
    const subServiceSpy =
      jasmine.createSpyObj('SubService', ['getValue']);
 
    const stubValue = 'stub value';
    subServiceSpy.getValue.and.returnValue(stubValue);
 
    masterService = new MasterService(subServiceSpy);
 
    expect(masterService.getValue())
      .toBe(stubValue, 'service returned stub value');
    expect(subServiceSpy.getValue.calls.count())
      .toBe(1, 'spy method was called once');
    expect(subServiceSpy.getValue.calls.mostRecent().returnValue)
      .toBe(stubValue);
  });
});

缺陷在于:當要注入的對象比較復雜的時候,這種方法就會力不從心。

方法二(推薦):

其實,Angular官方也考慮到了這個問題,提供了TestBed(來自@angular/core/testing)。TestBed 會動態創建一個用來模擬 @NgModule 的 Angular 測試模塊。

具體如下:

// file: master.service.spec.ts

describe('MasterService without Angular testing support', () => {
  let masterService: MasterService;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        MasterService,
        SubService
      ]
    });
    
    //從TestBed獲取一個masterService的實例,不必關心SubService,Angular會自動注入
    masterService = TestBed.get(MasterService);
  });
 
  it('#getValue should return real value from the real service', () => {
    expect(masterService.getValue()).toBe('real value');
  });

});

TestBed.congigureTestingModule方法接受一個配置對象,其屬性與@NgModule類似(imports,providers,declarations)等,具體參見https://angular.cn/api/core/testing/TestModuleMetadata),模擬出一個@NgModule

在這個例子中,在provider中聲明了MasterServiceSubService,此時TestBed會管理他們的生命周期,在使用TestBed.get(MasterService)時,TestBed會幫助實例話具體的對象和構建依賴對象。

同樣的,在使用TestBed時,我們也可以使用spy對象,如下:

// file: master.service.spec.ts

describe('MasterService without Angular testing support', () => {
  let masterService: MasterService;

  
  beforeEach(() => {
    //創建一個spy,名字是SubService,并且提供getValue方法
    const spy = jasmine.createSpyObj('SubService', ['getValue']);
    TestBed.configureTestingModule({
      providers: [
        MasterService,
        { provide: SubService, useValue: spy }//使用spy的SubService
      ]
    });
    
    //在masterService實例話的時候,會取到spy的SubService對象
    masterService = TestBed.get(MasterService);
  });

});

彈珠測試

其次就是針對Rxjs的Observable的單元測試,模擬Observable的數據流和異步是很困難的一件事情。

傳統的做法是spy observable的值,并且在subscribe中使用done。

it('async with observable', (done: DoneFn) => {

  // the spy's most recent call returns the observable
  asyncMethod.calls.mostRecent().returnValue.subscribe(() => {
    // check the result here;
    done();
  });
});

但是不足之處在于無法模擬Observable的數據流,Rxjs提供的彈珠測試的方式,很好的解決了這個問題。它們可以使我們以同步且可靠的方式來測試異步操作。

下面用一個例子簡單介紹彈珠測試,更多詳細信息可以參考官方說明:https://cn.rx.js.org/manual/usage.html#h12。

import { cold, hot } from 'jasmine-marbles';

const input = hot('-a--b--c--', { a: 1, b: 2, c: 3 });
const expected = cold('-x--y--z--', { x: 10, y: 20, z: 30 });

expect(input.pipe(map(i => 10 * i ))).toBeObservable(expected);

首先,從jasmine-marbles中導入coldhot函數. coldhot都接受一個滿足彈珠語法的字符串,和一個可選的對象,分別返回TestHotObservableTestColdObservable對象(都繼承自Observable)。 區別在于hot方法返回的是一個表示已經被訂閱且在運行中的Observablecold返回的是暫未被訂閱的Observable。

在這個例子中,'-a--b--c—'的每一個字符都表示一幀,-表示一個空白幀,a,b,c分別表示在該幀的數據,他們的值是hot第二個參數對象里面對應key所對應的值,所以例子中的hot會產生一個,在第二幀發出1,第5幀發出2,第8幀發出3的Observable。同理,可以明白變量expected的意思。

最后,我們可以通過使用expecttoBeObservable來判斷經過處理的input這個Observable的值是否與expected的值相等。

參考文檔

TestBed和MarbleTest可以極大的提高Angular中UT編寫的簡便性,這里只是拋磚引玉舉了兩個簡答的例子,更多的高級功能,還是有待大家一起發掘。下面是一些重要的參考鏈接:

https://angular.cn/guide/testing

https://github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/testing/marble-testing.md

https://cn.rx.js.org/manual/usage.html

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容