为了检查你的服务是否正常工作,你可以专门为它们编写测试。
如果你要试验本指南中所讲的应用,请在浏览器中运行它或下载并在本地运行它。
服务往往是最容易进行单元测试的文件。下面是一些针对 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 测试实用工具可以让你轻松调查这些注入服务的行为。
你的应用依靠 Angular 的依赖注入(DI)来创建服务。当服务有依赖时,DI 会查找或创建这些被依赖的服务。如果该被依赖的服务还有自己的依赖,DI 也会查找或创建它们。
作为服务的消费者,你不应该关心这些。你不应该关心构造函数参数的顺序或它们是如何创建的。
作为服务的测试人员,你至少要考虑第一层的服务依赖,但当你用 TestBed
测试实用工具来提供和创建服务时,你可以让 Angular DI 来创建服务并处理构造函数的参数顺序。
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()
来为每一个 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 调用的数据服务通常会注入并委托给 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
)回调。确保你会同时提供这两个回调函数,以便捕获错误。如果不这样做就会产生一个异步的、没有被捕获的可观察对象的错误,测试运行器可能会把它归因于一个完全不相关的测试。
数据服务和 HttpClient
之间的扩展交互可能比较复杂,并且难以通过间谍进行模拟。
HttpClientTestingModule
可以让这些测试场景更易于管理。