前回、前々回に引き続き、Angularでユニットテスト作成する時のエラー対応です。
今回はNgRx関連のエラーです。
error properties: Object({ longStack: 'TypeError: Cannot read properties of undefined (reading 'xxxxx') at http://localhost:9876/_karma_webpack_/webpack:/src/app/koma/store/reducers/index.ts:xx:xxみたいなエラーが出てしまった場合の対応です。
・・・・ええ、言いたいことは分かります。
undefinedって出ているんだから、該当箇所見ればすぐ分かるじゃん。
って事ですよね?
まあ、仰る通りなのですが、エラーとなっている箇所は、コンポーネント側ではなく、NgRxのReducer(index.ts)で、エラーも微妙な位置なので、どう修正すれば良いのか、分からないんですよね・・・・。
ちなみに、コンポーネント側は、
[koma-confirm.component.ts]
export class KomaConfirmComponent implements OnInit, OnDestroy { komaConfirmSubscription: Subscription = new Subscription(); saveData: KomaViewSaveModel; constructor( private router: Router, public store: Store<fromKoma.State> ) {} ngOnInit(): void { // 画面間保持項目の退避 this.komaConfirmSubscription.add( this.store .pipe(select(fromKoma.getDataRegisterPost)) .subscribe(model => { if (model) { this.saveData = model; } else { this.router.navigateByUrl('/koma/home'); } }), ); } ngOnDestroy(): void { this.komaConfirmSubscription.unsubscribe(); } }となっています。
良くある登録系機能(入力→確認→完了)の「確認画面」を想定して頂くと分かりやすいです。
入力画面で入力された値をStoreに格納し、それを確認画面(のngOnInit)で取得しようとしています。
もし、Storeからデータを取得出来ない場合、不正な遷移を行ったと見做し、ホーム画面に遷移させています。
なので、
this.store .pipe(select(fromKoma.getDataRegisterPost))が何となく怪しいのは分かるのですが、Reducer(index.ts)側は、
[/src/app/koma/store/reducers/index.ts]
export const getKomaState = createSelector( getKomaFeatureState, (state: KomaFeatureState) => state.koma, // ←ここがエラーになってる。 ); export const getDataRegisterPost = createSelector( getKomaState, fromKoma.getDataRegisterPost, // ←せめてここでエラーになっていれば・・・・ );上記の「state.koma」の部分がエラーとなっていて、内容としては、
TypeError: Cannot read properties of undefined (reading 'koma')と出ています。
せめて、
fromKoma.getDataRegisterPost,の部分でエラーになっていれば、エラー原因の当たりがすぐにつくのですが・・・・
では、このエラーの対応をしていきたいと思います。
テストファイルの方を見ていきましょう。
[koma-confirm.component.spec.ts]
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { provideMockStore } from '@ngrx/store/testing'; import { KomaConfirmComponent } from './koma-confirm.component'; describe('KomaConfirmComponent', () => { let component: KomaConfirmComponent; let fixture: ComponentFixture<KomaConfirmComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [KomaConfirmComponent], providers: [ provideMockStore(), ], imports: [ RouterTestingModule, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(KomaConfirmComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });DIの設定だけはしている感じですね。
前回は、「StoreのdispatchでActionの実行をテストする」だったので、特に意識しなかったのですが、
Storeからデータを取得する、という場合は、「selector」と、取得するデータを定義する必要があります。
[koma-confirm.component.spec.ts]
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { provideMockStore } from '@ngrx/store/testing'; import * as fromKoma from '../../store/reducers'; import { KomaConfirmComponent } from './koma-confirm.component'; describe('KomaConfirmComponent', () => { let component: KomaConfirmComponent; let fixture: ComponentFixture<KomaConfirmComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [KomaConfirmComponent], providers: [ provideMockStore({ selectors: [ { selector: fromKoma.getDataRegisterPost, value: { komaId: '1', komaData: '1111', } }, ] }), ], imports: [ RouterTestingModule, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(KomaConfirmComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });
provideMockStoreの定義に引数として、selectorとvalueを設定しています。
これで、エラー(TypeError: Cannot read properties of undefined (reading 'xxxxx'))は出なくなると思います。
では、ちゃんとテストケースを追加していこうと思います。
まず、書き換えたいのが、
provideMockStoreの定義に引数として、selectorとvalueを設定している部分です。
今、そこを追加したのに・・・変えるんかい!って感じですね。
後々、テストケースが追加になった時に、selector内の値の入れ替えを楽にしたいので、
provideMockStoreの引数で設定するのではなく、テストクラス内で使えるように変えていこうと思います。
テストケースも追加していきます。
[koma-confirm.component.spec.ts]
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Router } from '@angular/router'; import { provideMockStore, MockStore } from '@ngrx/store/testing'; import * as fromKoma from '../../store/reducers'; import { KomaConfirmComponent } from './koma-confirm.component'; import { KomaViewSaveModel } from '../../models/koma'; describe('KomaConfirmComponent', () => { let component: KomaConfirmComponent; let fixture: ComponentFixture<KomaConfirmComponent>; // 全テストケースで利用出来るように、ここで定義 let store: MockStore<fromKoma.State>; let router: Router; let mockDataRegisterPostSelector; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [KomaConfirmComponent], providers: [ provideMockStore(), ], imports: [ RouterTestingModule, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(KomaConfirmComponent); // DI後のインスタンスを取得 router = TestBed.inject(Router); store = TestBed.inject(MockStore); // データの初期値を設定 mockDataRegisterPostSelector = store.overrideSelector( fromAccount.getDataRegisterPost, { komaId: '1', komaData: '1111' } ); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); describe('ngOnInitのテスト', () => { it('画面引き継ぎ情報が無い場合、ホーム画面へ遷移すること', () => { // 処理のモックを作成 const navigateByUrlSpy = spyOn(router, 'navigateByUrl'); // データにnullを設定 mockDataRegisterPostSelector.setResult(null); // テスト対象のメソッドを呼び出す component.ngOnInit(); // navigateByUrlが、'/koma/home'を引数として呼ばれているか確認 expect(navigateByUrlSpy).toHaveBeenCalledWith('/koma/home'); }); it('画面引き継ぎ情報がある場合、データを退避すること', () => { // 処理のモックを作成 const navigateByUrlSpy = spyOn(router, 'navigateByUrl'); // データを再設定 const formModel: KomaViewSaveModel = { komaId: '2', komaData: '2222' }; mockDataRegisterPostSelector.setResult(formModel); // テスト対象のメソッドを呼び出す component.ngOnInit(); // 退避されたデータの中身をチェック expect(component.saveData).not.toBeUndefined(); expect(component.saveData).not.toBeNull(); expect(component.saveData.komaId).toBe('2'); expect(component.saveData.komaData).toBe('2222'); // navigateByUrlが、呼ばれていないか確認 expect(navigateByUrlSpy).not.toHaveBeenCalled(); }); }); });
ポイントとしては、「overrideSelector」によって、selectorを再定義している部分
mockDataRegisterPostSelector = store.overrideSelector( // (中略) );と、値を再設定する
mockDataRegisterPostSelector.setResult(formModel);の部分ですね。
これで、Store内のデータを設定し、テストをすることが可能になりました!
今後も、Angularでユニットテストについて、ブログに書いて行こうと思います。
Angular
0 件のコメント:
コメントを投稿