填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫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.

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:

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.

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.

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

或者,如果你喜歡把這個服務作為設定程式碼的一部分進行注入,也可以在 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);
});
    

測試帶依賴的服務時,需要在 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>;
});
    

該測試會像以前一樣使用該間諜。

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

沒有 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().

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

注意測試中是如何使用解構賦值來提取所需的設定變數的。

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

      
      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.

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 )回呼(Callback)。確保你會同時提供這兩個回呼(Callback)函式,以便捕獲錯誤。如果不這樣做就會產生一個非同步的、沒有被捕獲的可觀察物件的錯誤,測試執行器可能會把它歸因於一個完全不相關的測試。

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.