コンポーネントでは、ngrxセレクターを使用して状態のさまざまな部分を取得します。
public isListLoading$ = this.store.select(fromStore.getLoading);
public users$ = this.store.select(fromStore.getUsers);
fromStore.method
は、ngrx createSelector
メソッドを使用して作成されます。例えば:
export const getState = createFeatureSelector<UsersState>('users');
export const getLoading = createSelector(
getState,
(state: UsersState) => state.loading
);
テンプレートでこれらのオブザーバブルを使用します。
<div class="loader" *ngIf="isLoading$ | async"></div>
<ul class="userList">
<li class="userItem" *ngFor="let user of $users | async">{{user.name}}</li>
</div>
私は次のようなことができるテストを書きたいです:
store.select.and.returnValue(someSubject)
サブジェクトの値を変更し、これらの値に対してコンポーネントのテンプレートをテストできるようにします。
事実、私たちはそれをテストする適切な方法を見つけるのに苦労しています。コンポーネントでselect
メソッドが2つの異なるメソッド(MemoizedSelector)を引数として2回呼び出されるため、「andReturn」メソッドを記述する方法は?
実際のセレクターを使用したくないため、状態をモックしてから実際のセレクターを使用するのは適切な単体テスト方法ではないようです(テストは分離されず、実際のメソッドを使用してコンポーネントの動作をテストします)。
私は同じ課題にぶつかり、セレクターをサービスにラップすることでそれを一度解決したので、コンポーネントはストアを直接通過するのではなく、サービスを使用してデータを取得しました。これにより、コードがクリーンアップされ、テストが実装に依存せず、モックがはるかに簡単になりました。
mockUserService = {
get users$() { return of(mockUsers); },
get otherUserRelatedData$() { return of(otherMockData); }
}
TestBed.configureTestingModule({
providers: [{ provide: UserService, useValue: mockUserService }]
});
しかし、それをする前に、あなたの質問の問題を解決しなければなりませんでした。
解決策は、データを保存する場所によって異なります。 constructor
に保存する場合:
constructor(private store: Store) {
this.users$ = store.select(getUsers);
}
その後、store
によって返される値を変更するたびに、テストコンポーネントを再作成する必要があります。それを行うには、次の行に沿って関数を作成します。
const createComponent = (): MyComponent => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
return component;
};
そして、あなたのストアスパイが返すものの値を変更した後にそれを呼び出します:
describe('test', () => {
it('should get users from the store', () => {
const users: User[] = [{username: 'BlackHoleGalaxy'}];
store.select.and.returnValue(of(users));
const cmp = createComponent();
// proceed with assertions
});
});
または、ngOnInit
に値を設定している場合:
constructor(private store: Store) {}
ngOnInit() {
this.users$ = this.store.select(getUsers);
}
コンポーネントを1回作成すると、ストアからの戻り値を変更するたびにngOnInit
を呼び出すだけで済むため、少し簡単になります。
describe('test', () => {
it('should get users from the store', () => {
const users: User[] = [{username: 'BlackHoleGalaxy'}];
store.select.and.returnValue(of(users));
component.ngOnInit();
// proceed with assertions
});
});
私はそのようなヘルパーを作成しました:
class MockStore {
constructor(public selectors: any[]) {
}
select(calledSelector) {
const filteredSelectors = this.selectors.filter(s => s.selector === calledSelector);
if (filteredSelectors.length < 1) {
throw new Error('Some selector has not been mocked');
}
return cold('a', {a: filteredSelectors[0].value});
}
}
そして今、私のテストは次のようになります。
const mock = new MockStore([
{
selector: selectEditMode,
value: initialState.editMode
},
{
selector: selectLoading,
value: initialState.isLoading
}
]);
it('should be initialized', function () {
const store = jasmine.createSpyObj('store', ['dispatch', 'select']);
store.select.and.callFake(selector => mock.select(selector));
const comp = new MyComponent(store);
comp.ngOnInit();
expect(comp.editMode$).toBeObservable(cold('a', {a: false}));
expect(comp.isLoading$).toBeObservable(cold('a', {a: false}));
});
また、この問題に遭遇し、セレクターをラップするためにサービスを使用することも私にとって選択肢ではありません。特にテスト目的だけでなく、サービスを置き換えるためにストアを使用しているためです。
したがって、私は次の(また完璧ではない)ソリューションを思いつきました。
私は、各コンポーネントと異なるアスペクトごとに異なる「ストア」を使用します。あなたの例では、次のStores&Statesを定義します。
export class UserStore extends Store<UserState> {}
export class LoadingStatusStore extends Store<LoadingStatusState> {}
そして、それらをUser-Componentに注入します。
constructor( private userStore: UserStore, private LoadingStatusStore:
LoadingStatusStore ) {}
それらをUser-Component-Test-Class内でモックします。
TestBed.configureTestingModule({
imports: [...],
declarations: [...],
providers: [
{ provide: UserStore, useClass: MockStore },
{ provide: LoadingStatusStore, useClass: MockStore }
]
}).compileComponents();
BeforeEach()またはit()テストメソッドにそれらを注入します:
beforeEach(
inject(
[UserStore, LoadingStatusStore],
(
userStore: MockStore<UserState>,
loadingStatusStore: MockStore<LoadingStatusState>
) => {...}
次に、それらを使用して、さまざまなパイプメソッドをスパイできます。
const userPipeSpy = spyOn(userStore, 'pipe').and.returnValue(of(user));
const loadingStatusPipeSpy = spyOn(loadingStatusStore, 'pipe')
.and.returnValue(of(false));
この方法の欠点は、1つのテスト方法でストアの状態の複数の部分をテストできないことです。しかし今のところ、これは私の回避策として機能します。
セレクターをサービスに移動しても、セレクター自体をテストする場合、セレクターをモックする必要はありません。 ngrxには独自のモック方法があり、ここで説明します。 https://ngrx.io/guide/store/testing
次のようなものを使用できます。
spyOn(store, 'select').and.callFake(selectFake);
function pipeFake(op1: OperatorFunction<UsersState, any>): Observable<any> {
if (op1.toString() === fromStore.getLoading.toString()) {
return of(true);
}
if (op1.toString() === fromStore.getUsers.toString()) {
return of(fakeUsers);
}
return of({});
}