オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。
前回、
前々回に引き続き、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