Как насмехать селектор 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