狛ログ

2022年4月28日木曜日

Angular ユニットテスト エラー( NullInjectorError: No provider for Router )の対応。

オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

突然ですが、Angularでユニットテストしてますか?
CLIでコンポーネントを作ると、「xxxxxx.component.spec.ts」が作成されますが、
「そのまま何も変えない(ユニットテスト使わない) or 邪魔だから消す」と、なっているかもしれないですね・・・・
まあ、Angularのユニットテスト、割と難しいので、気持ちは分かります。

という事で、Angularでユニットテストを書いていく上で、良く出るエラーの対処方法を記載していこうと思います。
今回はDI関連のエラーです。

NullInjectorError: R3InjectorError(DynamicTestModule)[Router -> Router]: 
  NullInjectorError: No provider for Router!
みたいなエラーが出てしまった場合の対応ですね。
メッセージそのままですが、Router絡みのエラーであることが分かります。

Angularでは、DI可能なクラスであれば、コンポーネントのコンストラクタに定義する事で、DIして使用可能になります。
[hoge.component.ts]
import { Router } from '@angular/router';
// (中略)
  constructor(private router: Router) {}
// (中略)
  onClickBack(): void {
  	// 「戻る」ボタンを押したら、「Top画面」に遷移する
    this.router.navigateByUrl('/hoge/top');
  }

・・・つまりテストクラス側でもDIをする必要があるって事ですね
[hoge.component.spec.ts]
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { HogeComponent } from './hoge.component';

describe('HogeComponent', () => {
  let component: HogeComponent;
  let fixture: ComponentFixture<HogeComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [HogeComponent],
      imports: [
        RouterTestingModule,
      ],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(HogeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
って感じになります。
デフォルト(CLIによって作成された状態)から、
import { RouterTestingModule } from '@angular/router/testing';
と、
      imports: [
        RouterTestingModule,
      ],
を追加しています。

先ほどのエラー(NullInjectorError)を無くすだけであれば、実は上記でOKです。

ただ、当然テストケースは作らないといけないので、ついでにやってみましょう
[hoge.component.spec.ts]
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
// Routerをimport
import { Router } from '@angular/router';
import { HogeComponent } from './hoge.component';

describe('HogeComponent', () => {
  let component: HogeComponent;
  let fixture: ComponentFixture<HogeComponent>;
  // 全テストケースで利用出来るように、ここで定義
  let router: Router;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [HogeComponent],
      imports: [
        RouterTestingModule,
      ],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(HogeComponent);

    // DI後のインスタンスを取得
    router = TestBed.inject(Router);

    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  describe('ボタンアクションテスト', () => {
    it('戻るボタン押下時  ホーム画面へ遷移する事', () => {
      // navigateByUrl処理のモックを作成
      const navigateByUrlSpy = spyOn(router, 'navigateByUrl');

      // テスト対象のメソッドを呼び出す
      component.onClickBack();

      // navigateByUrlが、'/hoge/top'を引数として呼ばれているか確認
      expect(navigateByUrlSpy).toHaveBeenCalledWith('/hoge/top');
    });
  });
});

ポイントとしては、DIしたクラスをテスト側で使用する為には、
    router = TestBed.inject(Router);
が必要、というところです。
実際のテストの部分は、プログラム内のコメントを参照して頂ければと思いますが、流れ的に、

① spyOn で、navigateByUrl のモックを作成
② コンポーネント側の onClickBack を呼び出す
③ expect の toHaveBeenCalledWith で、指定の引数で navigateByUrl が呼ばれたかを判定

となっています。

ちょっと長くなってしまったので、その他のDI関連のテストエラーは別途記事にしようと思います。

0 件のコメント:

コメントを投稿