填写这份《一分钟调查》,帮我们(开发组)做得更好!去填写Home

测试服务

Testing services

为了检查你的服务是否正常工作,你可以专门为它们编写测试。

To check that your services are working as you intend, you can write tests specifically for them.

对于本测试指南中描述的范例应用,参阅范例应用范例应用

For the sample app that the testing guides describe, see thesample appsample app.

要了解本测试指南中涉及的测试特性,请参阅teststests

For the tests features in the testing guides, seeteststests.

服务往往是最容易进行单元测试的文件。下面是一些针对 ValueService 的同步和异步单元测试,甚至不需要 Angular 测试工具的帮助。

Services are often the easiest files to unit test. Here are some synchronous and asynchronous unit tests of the ValueService written without assistance from Angular testing utilities.

// 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(); }); }); });
app/demo/demo.spec.ts
      
      // 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();
    });
  });
});
    

有依赖的服务

Services with dependencies

服务通常依赖于 Angular 在构造函数中注入的其它服务。在很多情况下,调用服务的构造函数时,很容易手动创建和注入这些依赖。

Services often depend on other services that Angular injects into the constructor. In many cases, it's easy to create and inject these dependencies by hand while calling the service's constructor.

MasterService 就是一个简单的例子:

The MasterService is a simple example:

@Injectable() export class MasterService { constructor(private valueService: ValueService) { } getValue() { return this.valueService.getValue(); } }
app/demo/demo.ts
      
      @Injectable()
export class MasterService {
  constructor(private valueService: ValueService) { }
  getValue() { return this.valueService.getValue(); }
}
    

MasterService 只把它唯一的方法 getValue 委托给了所注入的 ValueService

MasterService delegates its only method, getValue, to the injected ValueService.

这里有几种测试方法。

Here are several ways to test it.

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()) .toBe(stubValue, 'service returned stub value'); expect(valueServiceSpy.getValue.calls.count()) .toBe(1, 'spy method was called once'); expect(valueServiceSpy.getValue.calls.mostRecent().returnValue) .toBe(stubValue); }); });
app/demo/demo.spec.ts
      
      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())
      .toBe(stubValue, 'service returned stub value');
    expect(valueServiceSpy.getValue.calls.count())
      .toBe(1, 'spy method was called once');
    expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
      .toBe(stubValue);
  });
});
    

第一个测试使用 new 创建了一个 ValueService,并把它传给了 MasterService 的构造函数。

The first test creates a ValueService with new and passes it to the MasterService constructor.

然而,注入真实服务很难工作良好,因为大多数被依赖的服务都很难创建和控制。

However, injecting the real service rarely works well as most dependent services are difficult to create and control.

相反,你可以模拟依赖、使用仿制品,或者在相关的服务方法上创建一个测试间谍

Instead you can mock the dependency, use a dummy value, or create a spy on the pertinent service method.

我更喜欢用测试间谍,因为它们通常是模拟服务的最简单方式。

Prefer spies as they are usually the easiest way to mock services.

这些标准的测试技巧非常适合对服务进行单独测试。

These standard testing techniques are great for unit testing services in isolation.

但是,你几乎总是使用 Angular 依赖注入机制来将服务注入到应用类中,你应该有一些测试来体现这种使用模式。 Angular 测试实用工具可以让你轻松调查这些注入服务的行为。

However, you almost always inject services into application classes using Angular dependency injection and you should have tests that reflect that usage pattern. Angular testing utilities make it easy to investigate how injected services behave.

使用 TestBed 测试服务

Testing services with the TestBed

你的应用依靠 Angular 的依赖注入(DI)来创建服务。当服务有依赖时,DI 会查找或创建这些被依赖的服务。如果该被依赖的服务还有自己的依赖,DI 也会查找或创建它们。

Your app relies on Angular dependency injection (DI) to create services. When a service has a dependent service, DI finds or creates that dependent service. And if that dependent service has its own dependencies, DI finds-or-creates them as well.

作为服务的消费者,你不应该关心这些。你不应该关心构造函数参数的顺序或它们是如何创建的。

As service consumer, you don't worry about any of this. You don't worry about the order of constructor arguments or how they're created.

作为服务的测试人员,你至少要考虑第一层的服务依赖,但当你用 TestBed 测试实用工具来提供和创建服务时,你可以让 Angular DI 来创建服务并处理构造函数的参数顺序。

As a service tester, you must at least think about the first level of service dependencies but you can let Angular DI do the service creation and deal with constructor argument order when you use the TestBed testing utility to provide and create services.

Angular TestBed

TestBed 是 Angular 测试实用工具中最重要的。 TestBed 创建了一个动态构造的 Angular 测试模块,用来模拟一个 Angular 的 @NgModule

The TestBed is the most important of the Angular testing utilities. The TestBed creates a dynamically-constructed Angular test module that emulates an Angular @NgModule.

TestBed.configureTestingModule() 方法接受一个元数据对象,它可以拥有@NgModule的大部分属性。

The TestBed.configureTestingModule() method takes a metadata object that can have most of the properties of an @NgModule.

要测试某个服务,你可以在元数据属性 providers 中设置一个要测试或模拟的服务数组。

To test a service, you set the providers metadata property with an array of the services that you'll test or mock.

let service: ValueService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ValueService] }); });
app/demo/demo.testbed.spec.ts (provide ValueService in beforeEach)
      
      let service: ValueService;

beforeEach(() => {
  TestBed.configureTestingModule({ providers: [ValueService] });
});
    

将服务类作为参数调用 TestBed.inject(),将它注入到测试中。

Then inject it inside a test by calling TestBed.inject() with the service class as the argument.

注意: TestBed.get() 已在 Angular 9 中弃用。为了帮助减少重大变更,Angular 引入了一个名为 TestBed.inject() 的新函数,你可以改用它。关于删除 TestBed.get() 的信息,请参阅弃用索引中的条目。

Note: TestBed.get() was deprecated as of Angular version 9. To help minimize breaking changes, Angular introduces a new function called TestBed.inject(), which you should use instead. For information on the removal of TestBed.get(), see its entry in the Deprecations index.

it('should use ValueService', () => { service = TestBed.inject(ValueService); expect(service.getValue()).toBe('real value'); });
      
      it('should use ValueService', () => {
  service = TestBed.inject(ValueService);
  expect(service.getValue()).toBe('real value');
});
    

或者,如果你喜欢把这个服务作为设置代码的一部分进行注入,也可以在 beforeEach() 中做。

Or inside the beforeEach() if you prefer to inject the service as part of your setup.

beforeEach(() => { TestBed.configureTestingModule({ providers: [ValueService] }); service = TestBed.inject(ValueService); });
      
      beforeEach(() => {
  TestBed.configureTestingModule({ providers: [ValueService] });
  service = TestBed.inject(ValueService);
});
    

测试带依赖的服务时,需要在 providers 数组中提供 mock。

When testing a service with a dependency, provide the mock in the providers array.

在下面的例子中,mock 是一个间谍对象。

In the following example, the mock is a spy object.

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>; });
      
      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>;
});
    

该测试会像以前一样使用该间谍。

The test consumes that spy in the same way it did earlier.

it('#getValue should return stubbed value from a spy', () => { const stubValue = 'stub value'; valueServiceSpy.getValue.and.returnValue(stubValue); expect(masterService.getValue()) .toBe(stubValue, 'service returned stub value'); expect(valueServiceSpy.getValue.calls.count()) .toBe(1, 'spy method was called once'); expect(valueServiceSpy.getValue.calls.mostRecent().returnValue) .toBe(stubValue); });
      
      it('#getValue should return stubbed value from a spy', () => {
  const stubValue = 'stub value';
  valueServiceSpy.getValue.and.returnValue(stubValue);

  expect(masterService.getValue())
    .toBe(stubValue, 'service returned stub value');
  expect(valueServiceSpy.getValue.calls.count())
    .toBe(1, 'spy method was called once');
  expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
    .toBe(stubValue);
});
    

没有 beforeEach() 的测试

Testing without beforeEach()

本指南中的大多数测试套件都会调用 beforeEach() 来为每一个 it() 测试设置前置条件,并依赖 TestBed 来创建类和注入服务。

Most test suites in this guide call beforeEach() to set the preconditions for each it() test and rely on the TestBed to create classes and inject services.

还有另一种测试,它们从不调用 beforeEach(),而是更喜欢显式地创建类,而不是使用 TestBed

There's another school of testing that never calls beforeEach() and prefers to create classes explicitly rather than use the TestBed.

你可以用这种风格重写 MasterService 中的一个测试。

Here's how you might rewrite one of the MasterService tests in that style.

首先,在 setup 函数中放入可供复用的预备代码,而不用 beforeEach()

Begin by putting re-usable, preparatory code in a setup function instead of 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 }; }
app/demo/demo.spec.ts (setup)
      
      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 )。

The setup() function returns an object literal with the variables, such as masterService, that a test might reference. You don't define semi-global variables (e.g., let masterService: MasterService) in the body of the describe().

然后,每个测试都会在第一行调用 setup(),然后继续执行那些操纵被测主体和断言期望值的步骤。

Then each test invokes setup() in its first line, before continuing with steps that manipulate the test subject and assert expectations.

it('#getValue should return stubbed value from a spy', () => { const { masterService, stubValue, valueServiceSpy } = setup(); expect(masterService.getValue()) .toBe(stubValue, 'service returned stub value'); expect(valueServiceSpy.getValue.calls.count()) .toBe(1, 'spy method was called once'); expect(valueServiceSpy.getValue.calls.mostRecent().returnValue) .toBe(stubValue); });
      
      it('#getValue should return stubbed value from a spy', () => {
  const { masterService, stubValue, valueServiceSpy } = setup();
  expect(masterService.getValue())
    .toBe(stubValue, 'service returned stub value');
  expect(valueServiceSpy.getValue.calls.count())
    .toBe(1, 'spy method was called once');
  expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
    .toBe(stubValue);
});
    

注意测试中是如何使用解构赋值来提取所需的设置变量的。

Notice how the test uses destructuring assignment to extract the setup variables that it needs.

const { masterService, stubValue, valueServiceSpy } = setup();
      
      const { masterService, stubValue, valueServiceSpy } = setup();
    

许多开发人员都觉得这种方法比传统的 beforeEach() 风格更清晰明了。

Many developers feel this approach is cleaner and more explicit than the traditional beforeEach() style.

虽然这个测试指南遵循传统的样式,并且默认的CLI 原理图会生成带有 beforeEach()TestBed 的测试文件,但你可以在自己的项目中采用这种替代方式

Although this testing guide follows the traditional style and the default CLI schematics generate test files with beforeEach() and TestBed, feel free to adopt this alternative approach in your own projects.

测试 HTTP 服务

Testing HTTP services

对远程服务器进行 HTTP 调用的数据服务通常会注入并委托给 Angular 的 HttpClient服务进行 XHR 调用。

Data services that make HTTP calls to remote servers typically inject and delegate to the Angular HttpClientservice for XHR calls.

你可以测试一个注入了 HttpClient 间谍的数据服务,就像测试所有带依赖的服务一样。

You can test a data service with an injected HttpClient spy as you would test any service with a dependency.

let httpClientSpy: { get: jasmine.Spy }; let heroService: HeroService; beforeEach(() => { // TODO: spy on other methods too httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); heroService = new HeroService(httpClientSpy as any); }); it('should return expected heroes (HttpClient called once)', () => { const expectedHeroes: Hero[] = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]; httpClientSpy.get.and.returnValue(asyncData(expectedHeroes)); heroService.getHeroes().subscribe( heroes => expect(heroes).toEqual(expectedHeroes, 'expected heroes'), fail ); expect(httpClientSpy.get.calls.count()).toBe(1, 'one call'); }); it('should return an error when the server returns a 404', () => { const errorResponse = new HttpErrorResponse({ error: 'test 404 error', status: 404, statusText: 'Not Found' }); httpClientSpy.get.and.returnValue(asyncError(errorResponse)); heroService.getHeroes().subscribe( heroes => fail('expected an error, not heroes'), error => expect(error.message).toContain('test 404 error') ); });
app/model/hero.service.spec.ts (tests with spies)
      
      let httpClientSpy: { get: jasmine.Spy };
let heroService: HeroService;

beforeEach(() => {
  // TODO: spy on other methods too
  httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
  heroService = new HeroService(httpClientSpy as any);
});

it('should return expected heroes (HttpClient called once)', () => {
  const expectedHeroes: Hero[] =
    [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];

  httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));

  heroService.getHeroes().subscribe(
    heroes => expect(heroes).toEqual(expectedHeroes, 'expected heroes'),
    fail
  );
  expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
});

it('should return an error when the server returns a 404', () => {
  const errorResponse = new HttpErrorResponse({
    error: 'test 404 error',
    status: 404, statusText: 'Not Found'
  });

  httpClientSpy.get.and.returnValue(asyncError(errorResponse));

  heroService.getHeroes().subscribe(
    heroes => fail('expected an error, not heroes'),
    error  => expect(error.message).toContain('test 404 error')
  );
});
    

HeroService 方法会返回 Observables 。你必须订阅一个可观察对象(a)让它执行,(b)断言该方法成功或失败。

The HeroService methods return Observables. You must subscribe to an observable to (a) cause it to execute and (b) assert that the method succeeds or fails.

subscribe() 方法会接受成功( next )和失败( error )回调。确保你会同时提供这两个回调函数,以便捕获错误。如果不这样做就会产生一个异步的、没有被捕获的可观察对象的错误,测试运行器可能会把它归因于一个完全不相关的测试。

The subscribe() method takes a success (next) and fail (error) callback. Make sure you provide both callbacks so that you capture errors. Neglecting to do so produces an asynchronous uncaught observable error that the test runner will likely attribute to a completely different test.

HttpClientTestingModule

数据服务和 HttpClient 之间的扩展交互可能比较复杂,并且难以通过间谍进行模拟。

Extended interactions between a data service and the HttpClient can be complex and difficult to mock with spies.

HttpClientTestingModule 可以让这些测试场景更易于管理。

The HttpClientTestingModule can make these testing scenarios more manageable.

虽然本指南附带的范例代码演示了 HttpClientTestingModule,但是本页面还是要引用一下 Http 指南,那份指南中详细介绍了 HttpClientTestingModule

While the code sample accompanying this guide demonstrates HttpClientTestingModule, this page defers to the Http guide, which covers testing with the HttpClientTestingModule in detail.