為了檢查你的服務(wù)是否正常工作,你可以專門為它們編寫測試。
服務(wù)往往是最容易進行單元測試的文件。下面是一些針對 ?ValueService
?的同步和異步單元測試,甚至不需要 Angular 測試工具的幫助。
// Straight Jasmine testing without Angular's testing support
describe('ValueService', () => {
let service: ValueService;
beforeEach(() => { service = new ValueService(); });
it('#getValue should return real value', () => {
expect(service.getValue()).toBe('real value');
});
it('#getObservableValue should return value from observable',
(done: DoneFn) => {
service.getObservableValue().subscribe(value => {
expect(value).toBe('observable value');
done();
});
});
it('#getPromiseValue should return value from a promise',
(done: DoneFn) => {
service.getPromiseValue().then(value => {
expect(value).toBe('promise value');
done();
});
});
});
服務(wù)通常依賴于 Angular 在構(gòu)造函數(shù)中注入的其它服務(wù)。在很多情況下,調(diào)用服務(wù)的構(gòu)造函數(shù)時,很容易手動創(chuàng)建和注入這些依賴。
?MasterService
?就是一個簡單的例子:
@Injectable()
export class MasterService {
constructor(private valueService: ValueService) { }
getValue() { return this.valueService.getValue(); }
}
?MasterService
?只把它唯一的方法 ?getValue
?委托給了所注入的 ?ValueService
?。
這里有幾種測試方法。
describe('MasterService without Angular testing support', () => {
let masterService: MasterService;
it('#getValue should return real value from the real service', () => {
masterService = new MasterService(new ValueService());
expect(masterService.getValue()).toBe('real value');
});
it('#getValue should return faked value from a fakeService', () => {
masterService = new MasterService(new FakeValueService());
expect(masterService.getValue()).toBe('faked service value');
});
it('#getValue should return faked value from a fake object', () => {
const fake = { getValue: () => 'fake value' };
masterService = new MasterService(fake as ValueService);
expect(masterService.getValue()).toBe('fake value');
});
it('#getValue should return stubbed value from a spy', () => {
// create `getValue` spy on an object representing the ValueService
const valueServiceSpy =
jasmine.createSpyObj('ValueService', ['getValue']);
// set the value to return when the `getValue` spy is called.
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
masterService = new MasterService(valueServiceSpy);
expect(masterService.getValue())
.withContext('service returned stub value')
.toBe(stubValue);
expect(valueServiceSpy.getValue.calls.count())
.withContext('spy method was called once')
.toBe(1);
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
});
第一個測試使用 ?new
?創(chuàng)建了一個 ?ValueService
?,并把它傳給了 ?MasterService
?的構(gòu)造函數(shù)。
然而,注入真實服務(wù)很難工作良好,因為大多數(shù)被依賴的服務(wù)都很難創(chuàng)建和控制。
相反,可以模擬依賴、使用仿制品,或者在相關(guān)的服務(wù)方法上創(chuàng)建一個測試間諜。
我更喜歡用測試間諜,因為它們通常是模擬服務(wù)的最佳途徑。
這些標(biāo)準(zhǔn)的測試技巧非常適合對服務(wù)進行單獨測試。
但是,你幾乎總是使用 Angular 依賴注入機制來將服務(wù)注入到應(yīng)用類中,你應(yīng)該有一些測試來體現(xiàn)這種使用模式。Angular 測試實用工具可以讓你輕松調(diào)查這些注入服務(wù)的行為。
你的應(yīng)用依靠 Angular 的依賴注入(DI)來創(chuàng)建服務(wù)。當(dāng)服務(wù)有依賴時,DI 會查找或創(chuàng)建這些被依賴的服務(wù)。如果該被依賴的服務(wù)還有自己的依賴,DI 也會查找或創(chuàng)建它們。
作為服務(wù)的消費者,你不應(yīng)該關(guān)心這些。你不應(yīng)該關(guān)心構(gòu)造函數(shù)參數(shù)的順序或它們是如何創(chuàng)建的。
作為服務(wù)的測試人員,你至少要考慮第一層的服務(wù)依賴,但當(dāng)你用 ?TestBed
?測試實用工具來提供和創(chuàng)建服務(wù)時,你可以讓 Angular DI 來創(chuàng)建服務(wù)并處理構(gòu)造函數(shù)的參數(shù)順序。
?TestBed
?是 Angular 測試實用工具中最重要的。?TestBed
?創(chuàng)建了一個動態(tài)構(gòu)造的 Angular 測試模塊,用來模擬一個 Angular 的 ?@NgModule
?。
?TestBed.configureTestingModule()
? 方法接受一個元數(shù)據(jù)對象,它可以擁有?@NgModule
?的大部分屬性。
要測試某個服務(wù),你可以在元數(shù)據(jù)屬性 ?providers
?中設(shè)置一個要測試或模擬的服務(wù)數(shù)組。
let service: ValueService;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
將服務(wù)類作為參數(shù)調(diào)用 ?TestBed.inject()
?,將它注入到測試中。
注意:
?TestBed.get()
? 已在 Angular 9 中棄用。為了幫助減少重大變更,Angular 引入了一個名為 ?TestBed.inject()
? 的新函數(shù),你可以改用它。
it('should use ValueService', () => {
service = TestBed.inject(ValueService);
expect(service.getValue()).toBe('real value');
});
或者,如果你喜歡把這個服務(wù)作為設(shè)置代碼的一部分進行注入,也可以在 ?beforeEach()
? 中做。
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
service = TestBed.inject(ValueService);
});
測試帶依賴的服務(wù)時,需要在 ?providers
?數(shù)組中提供 mock。
在下面的例子中,mock 是一個間諜對象。
let masterService: MasterService;
let valueServiceSpy: jasmine.SpyObj<ValueService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('ValueService', ['getValue']);
TestBed.configureTestingModule({
// Provide both the service-to-test and its (spy) dependency
providers: [
MasterService,
{ provide: ValueService, useValue: spy }
]
});
// Inject both the service-to-test and its (spy) dependency
masterService = TestBed.inject(MasterService);
valueServiceSpy = TestBed.inject(ValueService) as jasmine.SpyObj<ValueService>;
});
該測試會像以前一樣使用該間諜。
it('#getValue should return stubbed value from a spy', () => {
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
expect(masterService.getValue())
.withContext('service returned stub value')
.toBe(stubValue);
expect(valueServiceSpy.getValue.calls.count())
.withContext('spy method was called once')
.toBe(1);
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
本指南中的大多數(shù)測試套件都會調(diào)用 ?beforeEach()
? 來為每一個 ?it()
? 測試設(shè)置前置條件,并依賴 ?TestBed
?來創(chuàng)建類和注入服務(wù)。
還有另一種測試,它們從不調(diào)用 ?beforeEach()
?,而是更喜歡顯式地創(chuàng)建類,而不是使用 ?TestBed
?。
你可以用這種風(fēng)格重寫 ?MasterService
?中的一個測試。
首先,在 setup 函數(shù)中放入可供復(fù)用的預(yù)備代碼,而不用 ?beforeEach()
?。
function setup() {
const valueServiceSpy =
jasmine.createSpyObj('ValueService', ['getValue']);
const stubValue = 'stub value';
const masterService = new MasterService(valueServiceSpy);
valueServiceSpy.getValue.and.returnValue(stubValue);
return { masterService, stubValue, valueServiceSpy };
}
?setup()
? 函數(shù)返回一個包含測試可能引用的變量(如 ?masterService
?)的對象字面量。你并沒有在 ?describe()
? 的函數(shù)體中定義半全局變量(比如 ?let masterService: MasterService
?)。
然后,每個測試都會在第一行調(diào)用 ?setup()
?,然后繼續(xù)執(zhí)行那些操縱被測主體和斷言期望值的步驟。
it('#getValue should return stubbed value from a spy', () => {
const { masterService, stubValue, valueServiceSpy } = setup();
expect(masterService.getValue())
.withContext('service returned stub value')
.toBe(stubValue);
expect(valueServiceSpy.getValue.calls.count())
.withContext('spy method was called once')
.toBe(1);
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
請注意測試如何使用解構(gòu)賦值來提取它需要的設(shè)置變量。
const { masterService, stubValue, valueServiceSpy } = setup();
許多開發(fā)人員都覺得這種方法比傳統(tǒng)的 ?beforeEach()
? 風(fēng)格更清晰明了。
雖然這個測試指南遵循傳統(tǒng)的樣式,并且默認(rèn)的CLI 原理圖會生成帶有 ?beforeEach()
? 和 ?TestBed
?的測試文件,但你可以在自己的項目中采用這種替代方式。
對遠(yuǎn)程服務(wù)器進行 HTTP 調(diào)用的數(shù)據(jù)服務(wù)通常會注入并委托給 Angular 的 ?HttpClient
?服務(wù)進行 XHR 調(diào)用。
你可以測試一個注入了 ?HttpClient
?間諜的數(shù)據(jù)服務(wù),就像測試所有帶依賴的服務(wù)一樣。
let httpClientSpy: jasmine.SpyObj<HttpClient>;
let heroService: HeroService;
beforeEach(() => {
// TODO: spy on other methods too
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
heroService = new HeroService(httpClientSpy);
});
it('should return expected heroes (HttpClient called once)', (done: DoneFn) => {
const expectedHeroes: Hero[] =
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));
heroService.getHeroes().subscribe({
next: heroes => {
expect(heroes)
.withContext('expected heroes')
.toEqual(expectedHeroes);
done();
},
error: done.fail
});
expect(httpClientSpy.get.calls.count())
.withContext('one call')
.toBe(1);
});
it('should return an error when the server returns a 404', (done: DoneFn) => {
const errorResponse = new HttpErrorResponse({
error: 'test 404 error',
status: 404, statusText: 'Not Found'
});
httpClientSpy.get.and.returnValue(asyncError(errorResponse));
heroService.getHeroes().subscribe({
next: heroes => done.fail('expected an error, not heroes'),
error: error => {
expect(error.message).toContain('test 404 error');
done();
}
});
});
?HeroService
?方法會返回 ?Observables
?。你必須訂閱一個可觀察對象(a)讓它執(zhí)行,(b)斷言該方法成功或失敗。
?subscribe()
? 方法會接受成功(?next
?)和失敗(?error
?)回調(diào)。確保你會同時提供這兩個回調(diào)函數(shù),以便捕獲錯誤。如果不這樣做就會產(chǎn)生一個異步的、沒有被捕獲的可觀察對象的錯誤,測試運行器可能會把它歸因于一個完全不相關(guān)的測試。
數(shù)據(jù)服務(wù)和 ?HttpClient
?之間的擴展交互可能比較復(fù)雜,并且難以通過間諜進行模擬。
?HttpClientTestingModule
?可以讓這些測試場景更易于管理。
更多建議: