Angular 测试服务
测试服务
为了检查你的服务是否正常工作,你可以专门为它们编写测试。
服务往往是最容易进行单元测试的文件。下面是一些针对 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();
});
});
});
有依赖的服务
服务通常依赖于 Angular 在构造函数中注入的其它服务。在很多情况下,调用服务的构造函数时,很容易手动创建和注入这些依赖。
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
创建了一个 ValueService
,并把它传给了 MasterService
的构造函数。
然而,注入真实服务很难工作良好,因为大多数被依赖的服务都很难创建和控制。
相反,可以模拟依赖、使用仿制品,或者在相关的服务方法上创建一个测试间谍。
我更喜欢用测试间谍,因为它们通常是模拟服务的最佳途径。
这些标准的测试技巧非常适合对服务进行单独测试。
但是,你几乎总是使用 Angular 依赖注入机制来将服务注入到应用类中,你应该有一些测试来体现这种使用模式。Angular 测试实用工具可以让你轻松调查这些注入服务的行为。
使用 TestBed 测试服务
你的应用依靠 Angular 的依赖注入(DI)来创建服务。当服务有依赖时,DI 会查找或创建这些被依赖的服务。如果该被依赖的服务还有自己的依赖,DI 也会查找或创建它们。
作为服务的消费者,你不应该关心这些。你不应该关心构造函数参数的顺序或它们是如何创建的。
作为服务的测试人员,你至少要考虑第一层的服务依赖,但当你用 TestBed
测试实用工具来提供和创建服务时,你可以让 Angular DI 来创建服务并处理构造函数的参数顺序。
Angular TestBed
TestBed
是 Angular 测试实用工具中最重要的。TestBed
创建了一个动态构造的 Angular 测试模块,用来模拟一个 Angular 的 @NgModule
。
TestBed.configureTestingModule()
方法接受一个元数据对象,它可以拥有@NgModule
的大部分属性。
要测试某个服务,你可以在元数据属性 providers
中设置一个要测试或模拟的服务数组。
let service: ValueService;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
将服务类作为参数调用 TestBed.inject()
,将它注入到测试中。
注意:
TestBed.get()
已在 Angular 9 中弃用。为了帮助减少重大变更,Angular 引入了一个名为 TestBed.inject()
的新函数,你可以改用它。
it('should use ValueService', () => {
service = TestBed.inject(ValueService);
expect(service.getValue()).toBe('real value');
});
或者,如果你喜欢把这个服务作为设置代码的一部分进行注入,也可以在 beforeEach()
中做。
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
service = TestBed.inject(ValueService);
});
测试带依赖的服务时,需要在 providers
数组中提供 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);
});
没有 beforeEach() 的测试
本指南中的大多数测试套件都会调用 beforeEach()
来为每一个 it()
测试设置前置条件,并依赖 TestBed
来创建类和注入服务。
还有另一种测试,它们从不调用 beforeEach()
,而是更喜欢显式地创建类,而不是使用 TestBed
。
你可以用这种风格重写 MasterService
中的一个测试。
首先,在 setup 函数中放入可供复用的预备代码,而不用 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()
函数返回一个包含测试可能引用的变量(如 masterService
)的对象字面量。你并没有在 describe()
的函数体中定义半全局变量(比如 let masterService: MasterService
)。
然后,每个测试都会在第一行调用 setup()
,然后继续执行那些操纵被测主体和断言期望值的步骤。
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);
});
请注意测试如何使用解构赋值来提取它需要的设置变量。
const { masterService, stubValue, valueServiceSpy } = setup();
许多开发人员都觉得这种方法比传统的 beforeEach()
风格更清晰明了。
虽然这个测试指南遵循传统的样式,并且默认的CLI 原理图会生成带有 beforeEach()
和 TestBed
的测试文件,但你可以在自己的项目中采用这种替代方式。
测试 HTTP 服务
对远程服务器进行 HTTP 调用的数据服务通常会注入并委托给 Angular 的 HttpClient
服务进行 XHR 调用。
你可以测试一个注入了 HttpClient
间谍的数据服务,就像测试所有带依赖的服务一样。
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)让它执行,(b)断言该方法成功或失败。
subscribe()
方法会接受成功(next
)和失败(error
)回调。确保你会同时提供这两个回调函数,以便捕获错误。如果不这样做就会产生一个异步的、没有被捕获的可观察对象的错误,测试运行器可能会把它归因于一个完全不相关的测试。
HttpClientTestingModule
数据服务和 HttpClient
之间的扩展交互可能比较复杂,并且难以通过间谍进行模拟。
HttpClientTestingModule
可以让这些测试场景更易于管理。