詳細ガイド
テスト

サービスのテスト

NOTE: このガイドはVitestに向けて更新中ですが、一部のコード例は現在Karma/Jasmine構文とAPIを使用しています。該当する場合、Vitestの同等のものを提供するよう積極的に取り組んでいます。

サービスが意図通りに動作していることを確認するには、サービス専用のテストを作成できます。

サービスは、多くの場合、ユニットテストを実行するのに最もスムーズなファイルです。 以下は、Angularテストユーティリティの助けを借りずに記述された ValueService の同期および非同期のユニットテストです。

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

TestBed を使用したサービスのテスト

アプリケーションは、Angular の 依存関係注入 (DI) に依存してサービスを作成します。 サービスが依存サービスを持っている場合、DI はその依存サービスを見つけたり、作成します。 さらに、その依存サービスに独自の依存関係がある場合、DI はそれらも探し出して作成します。

サービスの 消費者 として、あなたはこれらについて心配する必要はありません。 コンストラクター引数の順序や、それらがどのように作成されるかについて心配する必要はありません。

サービスの テスター として、少なくともサービス依存関係の最初のレベルについて考える必要はありますが、TestBed テストユーティリティを使用してサービスを提供して作成し、コンストラクター引数の順序を処理するときは、Angular DI にサービスの作成を任せることができます。

Angular TestBed

TestBed は、Angular のテストユーティリティの中で最も重要なものです。 TestBed は、Angular の @NgModule をエミュレートする、動的に構築された Angular の テスト モジュールを作成します。

TestBed.configureTestingModule() メソッドは、@NgModule のほとんどのプロパティを持つことができるメタデータオブジェクトを受け取ります。

サービスをテストするには、テストまたはモックするサービスの配列を providers メタデータプロパティに設定します。

demo.testbed.spec.ts (beforeEach で ValueService を提供)

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

次に、サービスクラスを引数として TestBed.inject() を呼び出して、テスト内でサービスを注入します。

HELPFUL: 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 配列にモックを提供します。

次の例では、モックはスパイオブジェクトです。

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 のテストの1つを書き直す方法を以下に示します。

最初に、セットアップ 関数に、再利用可能な準備コードを beforeEach() の代わりに配置します。

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)は定義しません。

次に、各テストは、テスト対象の操作や期待の主張を続行する前に、最初の行で 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 スパイを使用して、データサービスをテストできます。

hero.service.spec.ts (スパイを使用したテスト)

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

IMPORTANT: HeroService メソッドは Observable を返します。 Observableに 登録 することで、(a) 実行させ、(b) メソッドが成功したか失敗したかをアサートする必要があります。

subscribe() メソッドは、成功 (next) と失敗 (error) のコールバックを受け取ります。 エラーを捕捉するために、両方の コールバックを提供してください。 これを怠ると、非同期でキャッチされないObservableエラーが発生し、テストランナーは別のテストによるエラーであると判断する可能性があります。

HttpClientTestingModule

データサービスと HttpClient の間の拡張されたやり取りは複雑で、スパイでモック化するのは難しい場合があります。

HttpClientTestingModule を使用すると、これらのテストシナリオをより管理しやすくなります。

このガイドに付属する コードサンプル では HttpClientTestingModule が示されていますが、このページでは、HttpClientTestingModule を使用したテストを詳しく説明している HTTP ガイド を参照します。