Как насмехать селектор ngrx в компоненте
В компоненте мы используем селектор 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)
чтобы иметь возможность изменять значение объекта и проверять шаблон компонента с этими значениями.
Дело в том, что мы пытаемся найти правильный способ проверить это. Как написать мой метод "andReturn", так как метод select
вызывается два раза в моем компоненте с двумя разными методами (MemoizedSelector) в качестве аргументов?
Мы не хотим использовать реальный селектор и поэтому высмеиваем состояние, а использование реального селектора, похоже, не является правильным способом unit test (тесты не будут изолированы и будут использовать реальные методы для проверки поведения компонента).
Ответы
Ответ 1
Я столкнулся с одним и тем же вызовом и решил его раз и навсегда, обернув мои селектора в сервисах, поэтому мои компоненты просто использовали эту услугу, чтобы получать свои данные, а не напрямую проходить через магазин. Я обнаружил, что это очистило мой код, сделало мои тесты реалистичными, а также стал намного глубже:
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);
}
Немного проще, поскольку вы можете создать компонент один раз и просто вызвать 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
});
});
Ответ 2
Я также столкнулся с этой проблемой, и использование сервисов для обертывания селекторов тоже для меня не вариант. Особенно не только для целей тестирования, а потому что я использую магазин для замены сервисов.
Поэтому я придумал следующее (тоже не идеальное) решение:
Я использую разные "Store" для каждого компонента и каждого аспекта. В вашем примере я бы определил следующие магазины и штаты:
export class UserStore extends Store<UserState> {}
export class LoadingStatusStore extends Store<LoadingStatusState> {}
И внедрить их в пользовательский компонент:
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));
Недостаток этого метода заключается в том, что вы все еще не можете протестировать более одной части состояния магазина в одном тестовом методе. Но пока это работает как обходной путь для меня.
Ответ 3
Я создал такой помощник:
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}));
});
Ответ 4
Вы можете использовать что-то вроде этого:
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({});
}
Ответ 5
Перемещение ваших селекторов в сервис не избавит вас от необходимости имитировать селекторы, если вы собираетесь тестировать сами селекторы. У ngrx теперь есть свой способ насмешки, и он описан здесь: https://ngrx.io/guide/store/testing