Angularテスト備忘録

Angularのテスト

公式サイトを見れば基礎的な内容はすべて書かれている。参考:Angular_リクエストのテスト

ポイントは、HttpTestingControllerを使用することで、通常のバックエンドではなく、テスト用のバックエンドにアクセスするようになること。

あとはこのテストコントローラーをどのように扱うか理解していれば実装できる。

flushとは何なのか

テストコードを書く上で一番理解しにくいのが、どのタイミングでHTTPクライアントの通信が実行されるのか?

通常のメソッドであればsubscribeやlastValueFromを呼び出したタイミングで実行されるが、テストの場合は厳密にはそうではない。

テストでは、リクエストの送信は通常通りのタイミングで行われるが、HTTPクライアントがレスポンスを返すのは、flushを呼び出すタイミングとなる。

リクエストをHttpTestingControllerに詰め込んで、flushで一斉にレスポンスをかえすイメージだ。

これがわかると、テストコードが書けるようになる。

HTTPクライアントのテスト

■service.ts(テストしたいメソッド)

HTTPリクエストで取得した文字列を2倍に連結して返すメソッドをテストする。

sampleFnc(): Observable {
  const res = this.http.get('API_URL').pipe(
    map((res) => {
      retrurn res + res;
    })
  )
}

■service.spec.ts(テストコード)

flushに着目して、HTTPクライアントテスト時の通信タイミングを整理してみる。

// 事前準備
beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [
      CompNameService,  // テストで使用するサービス
      // その他テストで使用するクラスを記述
    ],
    imports: [HttpClientTestingModule]
  }).compileComponents();
  // DI構成
  service = TestBed.inject(CompNameService);
  httpTesting = TestBed.inject(HttpTestingController);
});

// テスト
it('sampleFnc_SUCCESS', () => {
  // テストするメソッドのレスポンスを構成する
  const res = lastValueFrom(service.sampleFnc()); // …①
  // この時点で、HTTPクライアントに、リクエストは設定されるが、レスポンスはflushするまで返ってこない。
  // HTTPリクエストの検証をするために、expect(検証内容)より前にHTTPクライアントにリクエストを設定しておく必要がある。
  
  // httpTestingとexpectを使用して、検証する内容を構成する
  const req = httpTesting.expectOne({  // HTTPクライアントにリクエストを送信する…②
    url: 'API_URL',
    method: 'GET'
  });
  res.then((resStr) => {  // テストするメソッドから返されたres(…①)はPromiseなので、thenで値を取り出す…③
    expect(resStr).toEqual(resMock + resMock);  // resから取得した文字列が、テストメソッドの処理を通し、resMockを2倍に連結した値になっていることを確認する
  })
  
  // flushを実行することで、HTTPクライアントがレスポンスを返す。
  // flushの引数にオブジェクトを指定すると、 リクエスト構成に対し任意のレスポンスを設定できる
  resMock = 'テストレスポンス';  // "HTTPリクエスト"が返すレスポンスのMockを作成する
  req.flush(resMock); // HTTPリクエスト(…②)の返すレスポンスをここで設定する。③の検証は、このタイミングで実行される。

  httpTesting.verify(); // 未処理のリクエストが無いことを確認する
});

voidのテスト

■service.ts(テストしたいメソッド)

sampleFnc(): Observable {
  return this.http.get('API_URL');
}

■service.spec.ts(テストコード)

it('sampleFnc_SUCCESS', () => {
  const res = lastValueFrom(service.sampleFnc());  //この時点ではリクエストは実行されていない
  
  const req = httpTesting.expectOne({
    url: 'API_URL',
    method: 'GET'
  });
});

voidの場合、リクエストの検証ができればよいということであれば、flushする必要すら無い。

service.sampleFnc()を記述したタイミングで、httpTestingにリクエストがセットされているためである。

flushを記述するのは、レスポンスを検証したいから。

ただし、このとき、httpTesting.verify()を最後に記述するとエラーになるため注意する。

このメソッドはHTTPクライアントに、未完了のリクエストが無いかを検証しているので、レスポンスを返していない状態ではエラーとなる。

voidのレスポンスを検証したい場合は、リクエストの返却は以下のようにする。

req.flush({});

catchErrorのテスト

■service.ts(テストしたいメソッド)

sampleFnc(): Observable {
  return this.http.get('API_URL').pipe(
    catchError((err) => {
      return of(err);
    })
  )
}

■service.spec.ts(テストコード)

catchError内の処理が実行されているかを検証するには、flushで、レスポンスにエラーを設定する。

it('sampleFnc_SUCCESS', () => {
  const res = lastValueFrom(service.sampleFnc()).catch;  // エラーをキャッチする
  
  const req = httpTesting.expectOne({
    url: 'API_URL',
    method: 'GET'
  });
  
  // flushの引数で、レスポンスにエラーを設定する
  req.flush('Failed!', {status: 500, statusText: 'Internal Server Error'});
  expect(res).toThrowError;  // エラーをスローしていることを検証する
});
httpTesting.verify();

ストア・クエリのテスト

■service.ts(テストしたいメソッド)

ストアに正しく値を設定できているかを確認したい。

setParam(val :string): Observable { 
  this.store.update({
    storeParam: val
  })
}

■service.spec.ts(テストコード)

describe('テストグループ名', () => {
  let serviceName: ServiceName;
  let serviceName: QueryName;
  let serviceName: StoreName;
  
  // 事前処理
  beforeEach(() => {
    TestBed.configureTestringModule({
      providers: [
        ServiceName,
        QueryName,
        StoreName
      ],
      imports: [HttpClient TestingModule]
    });
    service = TestBed.inject(ServiceName);  // サービスをテストモジュールに設定
    query = TestBed.inject(QueryName);  // クエリをテストモジュールに設定
    store = TestBed.inject(StoreName);  // ストアをテストモジュールに設定
    // デフォルトとして用意しているテストデータがあれば以下記述
    const masterMock = TestBed.inject(MasterDataMockService);
    masterMock.initMockData();
  });
  
  // テスト実施
  it('テスト名', () => {
    const inputParam = new InputParam();
    inputParam.param = 'sample'; // テストしたいデータを設定
    serviceName.setParam(input Param); // テストしたいメソッド(setParam)を呼び出し>storeに格納されるので、queryから取得できるようになる
    expect(query.getValue().inputParam.param).toBe('sample'); // クエリから取得したいが期待値と一致するか確認する
  });
}

Cookieのテスト

ブラウザに設定されている’sample’という名前のcookieを取得し、リクエストヘッダーに設定するリクエストをテストしたい。

■service.ts(テストしたいメソッド)

sampleFnc(): Observable {
  const httpOptions = {
    headers: new HttpHeaders({
      cookieSample: this.cookieService.get('sample');
    })
  }
  return this.http.get('API_URL')
}

■service.spec.ts(テストコード)

// 事前にCookieを設定する
beforeEach(() => {
  TestBed.configureTestingModule({
      providers: [
        CookieService,
        …
      ],
      …
  }).compileComponents();
  …
  cookieService = TestBed.inject(CookieService);
  cookieService.set('sampleCookie', 'sampleCookieVal');
})

it('sampleFnc_getCookie_SUCCESS', () => {
  lastValueFrom(service.sampleFnc());
  
  const req = httpTesting.expectOne({
    url: 'API_URL',
    method: 'GET'
  });

  // beforEachで設定したCookieを、ヘッダーに設定できているかを確認する
  expect(req.request.headers.get('sample')).toEqual('sampleCookieVal');
});

Appendix

テストの起動

npm test

ファイル単位でのテストの起動

npm run test --include src/[filePath]/….spec.ts --code-coverage

外部API・フレームワークのMock

郵便番号からの住所の取得や、QRコードの生成など、テストできない外部機能はMockを事前に作成してレスポンスを指定しておく。

以下、①はActivatedRouteをMock化してDI、②はHTMLCanvasElementが呼ばれたときにMockを返す方法。

beforeEach(() => {
  const routerSpyObj = jasmine.createSpyObj('ActivatedRoute', ['snapshot']); // …①
  const mockCanvas = jasmine.createSpyObj('HTMLCanvasElement', ['getContext']); // …②ダミーのHTMLCanvasElementクラスを作成。このクラスはgetContextメソッドを持つ
  …
  TestBed.configureTestingModule({
    providers: [
      { provide: ActivatedRoute, useValue: routerSpyObj }, …①
      …
    ]
  }).compileComponents();
  routerSpy = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; // …①ActivatedRouteはMockとしてDIしておく
  sptyOn(document, 'createElement').and.returnValue(mockCanvas);  // …②documentクラスのcreateElementメソッドが呼ばれたときには、mockCanvasを返却する
})

テストのデバッグ方法

以下の設定を加える。

■karma.conf.js

module.exports=function(config){
  config.set({
    …
    autoWatch: false,
    browsers: ['Chrome'],
    singleRun: false
  })
}

■package.json

{
  "scripts": {
    "test": "--source-map=true"
  }
}

IEモードで開発者ツールを使用する

参考:EdgeのIEモードの画面に対して開発者ツール(F12)を使う方法

用語集

  • Jasmine:テストコードを効率よく記述するためのフレームワーク。Seleniumもこのレイヤーに位置する。
  • Karma:テストランナー。テストコードをブラウザで起動して(この理由から、テスト中はchrome.exeのコンソールが開く)、レポートする。

 

 

コメント

タイトルとURLをコピーしました