狛ログ

2022年4月28日木曜日

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

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

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

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

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

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

・・・つまりテストクラス側でもDIをする必要があるって事ですね
[hoge.component.spec.ts]
  1. import { ComponentFixture, TestBed } from '@angular/core/testing';
  2. import { RouterTestingModule } from '@angular/router/testing';
  3. import { HogeComponent } from './hoge.component';
  4.  
  5. describe('HogeComponent', () => {
  6. let component: HogeComponent;
  7. let fixture: ComponentFixture<HogeComponent>;
  8.  
  9. beforeEach(async () => {
  10. await TestBed.configureTestingModule({
  11. declarations: [HogeComponent],
  12. imports: [
  13. RouterTestingModule,
  14. ],
  15. }).compileComponents();
  16. });
  17.  
  18. beforeEach(() => {
  19. fixture = TestBed.createComponent(HogeComponent);
  20. component = fixture.componentInstance;
  21. fixture.detectChanges();
  22. });
  23.  
  24. it('should create', () => {
  25. expect(component).toBeTruthy();
  26. });
  27. });
って感じになります。
デフォルト(CLIによって作成された状態)から、
  1. import { RouterTestingModule } from '@angular/router/testing';
と、
  1. imports: [
  2. RouterTestingModule,
  3. ],
を追加しています。

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

ただ、当然テストケースは作らないといけないので、ついでにやってみましょう
[hoge.component.spec.ts]
  1. import { ComponentFixture, TestBed } from '@angular/core/testing';
  2. import { RouterTestingModule } from '@angular/router/testing';
  3. // Routerをimport
  4. import { Router } from '@angular/router';
  5. import { HogeComponent } from './hoge.component';
  6.  
  7. describe('HogeComponent', () => {
  8. let component: HogeComponent;
  9. let fixture: ComponentFixture<HogeComponent>;
  10. // 全テストケースで利用出来るように、ここで定義
  11. let router: Router;
  12.  
  13. beforeEach(async () => {
  14. await TestBed.configureTestingModule({
  15. declarations: [HogeComponent],
  16. imports: [
  17. RouterTestingModule,
  18. ],
  19. }).compileComponents();
  20. });
  21.  
  22. beforeEach(() => {
  23. fixture = TestBed.createComponent(HogeComponent);
  24.  
  25. // DI後のインスタンスを取得
  26. router = TestBed.inject(Router);
  27.  
  28. component = fixture.componentInstance;
  29. fixture.detectChanges();
  30. });
  31.  
  32. it('should create', () => {
  33. expect(component).toBeTruthy();
  34. });
  35.  
  36. describe('ボタンアクションテスト', () => {
  37. it('戻るボタン押下時  ホーム画面へ遷移する事', () => {
  38. // navigateByUrl処理のモックを作成
  39. const navigateByUrlSpy = spyOn(router, 'navigateByUrl');
  40.  
  41. // テスト対象のメソッドを呼び出す
  42. component.onClickBack();
  43.  
  44. // navigateByUrlが、'/hoge/top'を引数として呼ばれているか確認
  45. expect(navigateByUrlSpy).toHaveBeenCalledWith('/hoge/top');
  46. });
  47. });
  48. });

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

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

となっています。

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

0 件のコメント:

コメントを投稿