狛ログ

2022年5月13日金曜日

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

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

前回に引き続き、Angularでユニットテスト作成時のエラー対応です。
今回もDI関連のエラーです。
  1. NullInjectorError: R3InjectorError(DynamicTestModule)[Store -> Store]:
  2. NullInjectorError: No provider for Store!
上記のエラーが出てしまった場合の対応です。

これは、NgRxのStoreを使用している場合に出てくるエラーとなります。
NgRxの公式サイトを見ると、コンポーネント側のテスト記載のサンプルがあるので、詳細はそちらを見るとして、とりあえず、エラーを無くしてみます。
まずは、コンポーネント側のロジックを確認してみましょう。

例として、よくある「入力画面(から確認画面)」を挙げてみます。
「入力画面で、入力した値をSubmitによって、Storeに保存し、確認画面へ引き継ぐ」って感じですね。
[input.component.ts]
  1. import { Router } from '@angular/router';
  2. import { Store } from '@ngrx/store';
  3. import * as fromInput from '../../store/reducers';
  4. import * as InputActions from '../../store/actions/input.actions';
  5. import { InputViewSaveModel } from '../../models/input';
  6. // (中略)
  7. constructor(
  8. private router: Router,
  9. public store: Store<fromInput.State>,
  10. ) {}
  11. // (中略)
  12. onSubmit(inputData: InputViewSaveModel): void {
  13. // 入力情報をStoreに格納して(=引き継ぎ情報として)確認画面へ遷移
  14. this.store.dispatch(InputActions.setRegisterPostData({ data: inputData }));
  15. this.router.navigateByUrl('/koma/confirm');
  16. }

ここではNgRxの仕様については触れませんので、NgRxを知っている前提で進めます。
それと、前回やった「Router」もありますので、一緒にエラーが出ないように対応してみます。

まずは必要モジュールのインポートと、DIをします。
(Angularでは、DI可能なクラスであれば、コンポーネントのコンストラクタに定義する事で、DIして使用可能になります。)
[input.component.spec.ts]
  1. import { ComponentFixture, TestBed } from '@angular/core/testing';
  2. import { RouterTestingModule } from '@angular/router/testing';
  3. import { provideMockStore } from '@ngrx/store/testing';
  4. import { InputComponent } from './input.component';
  5.  
  6. describe('InputComponent', () => {
  7. let component: InputComponent;
  8. let fixture: ComponentFixture<InputComponent>;
  9.  
  10. beforeEach(async () => {
  11. await TestBed.configureTestingModule({
  12. declarations: [InputComponent],
  13. providers: [provideMockStore()],
  14. imports: [RouterTestingModule],
  15. }).compileComponents();
  16. });
  17.  
  18. beforeEach(() => {
  19. fixture = TestBed.createComponent(InputComponent);
  20. component = fixture.componentInstance;
  21. fixture.detectChanges();
  22. });
  23.  
  24. it('should create', () => {
  25. expect(component).toBeTruthy();
  26. });
  27. });
と言う感じになります。

Storeに関する部分は、
  1. import { provideMockStore } from '@ngrx/store/testing';
と、
  1. providers: [provideMockStore()],
を追加しています。
先ほどのエラー(NullInjectorError)を無くすだけであれば、実は上記でOKです。

ただ、当然テストケースは作らないといけないので、ついでにやってみましょう
[input.component.spec.ts]
  1. import { ComponentFixture, TestBed } from '@angular/core/testing';
  2. import { RouterTestingModule } from '@angular/router/testing';
  3. import { Router } from '@angular/router';
  4. import { provideMockStore, MockStore } from '@ngrx/store/testing';
  5. import * as fromInput from '../../store/reducers';
  6. import * as InputActions from '../../store/actions/input.actions';
  7. import { InputViewSaveModel } from '../../models/input';
  8. import { InputComponent } from './input.component';
  9.  
  10. describe('InputComponent', () => {
  11. let component: InputComponent;
  12. let fixture: ComponentFixture<InputComponent>;
  13. // 全テストケースで利用出来るように、ここで定義
  14. let router: Router;
  15. let store: MockStore<fromInput.State>;
  16.  
  17. beforeEach(async () => {
  18. await TestBed.configureTestingModule({
  19. declarations: [InputComponent],
  20. providers: [provideMockStore()],
  21. imports: [RouterTestingModule],
  22. }).compileComponents();
  23. });
  24.  
  25. beforeEach(() => {
  26. fixture = TestBed.createComponent(InputComponent);
  27.  
  28. // DI後のインスタンスを取得
  29. router = TestBed.inject(Router);
  30. store = TestBed.inject(MockStore);
  31.  
  32. component = fixture.componentInstance;
  33. fixture.detectChanges();
  34. });
  35.  
  36. it('should create', () => {
  37. expect(component).toBeTruthy();
  38. });
  39.  
  40. describe('ボタンアクションテスト', () => {
  41. it('確認ボタン押下時 Storeにデータを保存し、確認画面へ遷移する事', () => {
  42. // 処理のモックを作成
  43. const dispatchSpy = spyOn(store, 'dispatch');
  44. const navigateByUrlSpy = spyOn(router, 'navigateByUrl');
  45.  
  46. // 画面から入力された値を再現
  47. const formModel: InputViewSaveModel = {
  48. id: '111',
  49. name: 'koma-chan',
  50. address: 'tokyo',
  51. };
  52.  
  53. // 実行されるアクションを定義(画面からの入力値をStoreへ保存するアクション)
  54. const expectedAction = InputActions.setRegisterPostData({ data: formModel });
  55.  
  56. // テスト対象のメソッドを呼び出す
  57. component.onSubmit(formModel);
  58.  
  59. // Storeのdispatchが、アクションを引数として呼ばれているか確認
  60. expect(dispatchSpy).toHaveBeenCalledWith(expectedAction);
  61.  
  62. // navigateByUrlが、'/koma/confirm'を引数として呼ばれているか確認
  63. expect(navigateByUrlSpy).toHaveBeenCalledWith('/koma/confirm');
  64. });
  65. });
  66. });

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

① spyOn で、Store の dispatch に対するモックを作成
② 画面の入力値を Store に保存するアクションを定義
③ コンポーネント側の onSubmit を呼び出す
③ expect の toHaveBeenCalledWith で、指定の引数(アクション)で dispatch が呼ばれたかを判定

となっています。

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

0 件のコメント:

コメントを投稿