最近進入到新的項目,前端采用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中聲明了MasterService
和SubService
,此時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
中導入cold
和hot
函數. cold
和hot
都接受一個滿足彈珠語法的字符串,和一個可選的對象,分別返回TestHotObservable
和TestColdObservable
對象(都繼承自Observable
)。 區別在于hot方法返回的是一個表示已經被訂閱且在運行中的Observable
,cold
返回的是暫未被訂閱的Observable
。
在這個例子中,'-a--b--c—'
的每一個字符都表示一幀,-
表示一個空白幀,a,b,c
分別表示在該幀的數據,他們的值是hot第二個參數對象里面對應key所對應的值,所以例子中的hot會產生一個,在第二幀發出1,第5幀發出2,第8幀發出3的Observable。同理,可以明白變量expected的意思。
最后,我們可以通過使用expect
的toBeObservable
來判斷經過處理的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