NgRx Testing - обратный вызов подписки не обновляется во время теста - PullRequest
1 голос
/ 07 мая 2020

Я пытаюсь протестировать компонент, который редактирует элементы списка покупок.
При первой загрузке соответствующие значения состояния, которые он получает через подписку на store.select, следующие:

editedIngredient: null,
editedIngredientIndex: -1

С этими значениями , он гарантирует, что для свойства класса 'editMode установлено значение false.
Во время тестирования я пытаюсь обновить состояние после загрузки компонента.
Я пытаюсь достичь обновление свойств editedIngredient и editedIngredientIndex до истинного значения в моем компоненте, что позволяет установить свойство editMode на true.

Пробуя приведенный ниже код, я могу получить компонент для рендеринга, а editMode изначально установлен в false.
Однако после обновления состояния внутри моего теста подписчик store.select делает не обновляется, что означает, что тест просто завершается, а для editMode никогда не устанавливается значение true.

Код компонента (ShoppingEditComponent)

ngOnInit() {
  this._subscription = this.store.select('shoppingList').subscribe(stateData => {
    if (stateData.editedIngredientIndex > -1) {
      this.editMode = true; // I want to update my state later so that I set this value
      return;
    }
    this.editMode = false; // I start with this value
  });
}

Код теста

let store: MockStore<State>;
const initialState: State = {
  editedIngredient: null,
  editedIngredientIndex: -1
};
const updatedShoppingListState: State = {
  editedIngredient: seedData[0],
  editedIngredientIndex: 0
};

let storeMock;
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [FormsModule],
      declarations: [
        ShoppingEditComponent
      ],
      providers: [
        provideMockStore({ initialState }),
      ]
    });
});

Тест Попытка 1

it('should have \'editMode = true\' when it receives a selected ingredient in updated state',
  fakeAsync(() => {
    const fixture = TestBed.createComponent(ShoppingEditComponent);
    const componentInstance = fixture.componentInstance;

    // no change detection occurs, hence the subscribe callback does not get called with state update
    store.setState(updatedShoppingListState); 
    expect(componentInstance['editMode']).toEqual(true);
  })
);

Тестовая попытка 2

it('should have \'editMode = true\' when it receives a selected ingredient in updated state',
  fakeAsync((done) => {
    const fixture = TestBed.createComponent(ShoppingEditComponent);
    const componentInstance = fixture.componentInstance;
    fixture.whenStable().then(() => {
      store.setState(updatedShoppingListState);
      expect(componentInstance['editMode']).toEqual(true);
      done();
    });
  })
);
  • Проблема, с которой я столкнулся с первой попыткой, заключается в том, что обнаружение изменений не происходит.
  • Проблема, с которой я столкнулся с попыткой 2, заключается в том, что обнаружение изменений не происходит, если я опускаю обратный вызов done().
    Однако, если я включаю обратный вызов done(), я получаю сообщение об ошибке, указывающее, что done() is not a function

Для справки: я нашел пример имитации магазина из документов NgRx (я использую Angular 8, поэтому этот пример наиболее актуален для меня)

Я использую Карму / Жасмин для своих тестов.

Любые рекомендации будут действительно полезно.

Ответы [ 2 ]

2 голосов
/ 08 мая 2020

После некоторых исследований, думаю, я нашел проблему.

Давайте посмотрим на реализацию provideMockStore :

export function provideMockStore<T = any>(
  config: MockStoreConfig<T> = {}
): Provider[] {
  return [
    ActionsSubject,
    MockState,
    MockStore,
    { provide: INITIAL_STATE, useValue: config.initialState || {} },
    { provide: MOCK_SELECTORS, useValue: config.selectors },
    { provide: StateObservable, useClass: MockState },
    { provide: ReducerManager, useClass: MockReducerManager },
    { provide: Store, useExisting: MockStore },
  ];
}

Объект config, который может быть передан в provideMockStore, имеет следующую форму:

export interface MockStoreConfig<T> {
  initialState?: T;
  selectors?: MockSelector[];
}

Как видите, значение в config.initialState равно присвоено токену INITIAL_STATE , который далее вводится в Store (MockStore в данном случае).

Обратите внимание, как вы его предоставляете:

const initialState: State = {
  editedIngredient: null,
  editedIngredientIndex: -1
};

provideStore({ initialState })

Это означает, что INITIAL_STATE будет этим :

{
  editedIngredient: null,
  editedIngredientIndex: -1
};

Вот как MockStore выглядит:

 constructor(
    private state$: MockState<T>,
    actionsObserver: ActionsSubject,
    reducerManager: ReducerManager,
    @Inject(INITIAL_STATE) private initialState: T,
    @Inject(MOCK_SELECTORS) mockSelectors: MockSelector[] = []
  ) {
    super(state$, actionsObserver, reducerManager);
    this.resetSelectors();
    this.setState(this.initialState);
    this.scannedActions$ = actionsObserver.asObservable();
    for (const mockSelector of mockSelectors) {
      this.overrideSelector(mockSelector.selector, mockSelector.value);
    }
  }

Обратите внимание, что он вводит INITIAL_STATE. MockState - это просто BehaviorSubject. Позвонив по номеру super(state$, actionsObserver, reducerManager);, вы убедитесь, что когда вы сделаете this.store.pipe() в своем компоненте, вы получите значение MockState.

Вот как вы выбираете из магазина:

this.store.select('shoppingList').pipe(...)

но ваше начальное состояние выглядит так:

{
  editedIngredient: null,
  editedIngredientIndex: -1
};

Имея это в виду, я думаю, вы могли бы решить проблему, если бы вы это сделали:

const initialState = {
  editedIngredient: null,
  editedIngredientIndex: -1
};

provideMockStore({ initialState: { shoppingList: initialState } })

Кроме того, если вы хотите глубже погрузиться в ngrx/store, вы можете проверить эту статью .

0 голосов
/ 10 мая 2020

Мне удалось добиться обновления подписчика store.select благодаря вкладу Андрея.
Мне нужно было сделать следующее:

import * as fromApp from '../../store/app.reducer';

const seedData = getShoppingListSeedData();
const mockInitialAppState: fromApp.AppState = {
  shoppingList: {
    ingredients: seedData,
    editedIngredient: null,
    editedIngredientIndex: -1
  },
  ... default state of other store types' defined in AppState are also provided
};

describe('When an item is selected', () => {
/* In the question code, I was passing the ShoppingList 'State' type into MockStore<T>,  
   whereas it should have been the AppState, as we see on the next line 
*/
    let store: MockStore<fromApp.AppState>;

    const shoppingListState: ShoppingListState = {
      ingredients: seedData,
      editedIngredient: seedData[0],
      editedIngredientIndex: 0
    };

    beforeEach(() => {
      TestBed.configureTestingModule({
        imports: [FormsModule],
        declarations: [ShoppingEditComponent],
        providers: [
          // this ensures that "shoppingList: initialState" is part of the initial state, as per Andrei's suggestion
          provideMockStore({ initialState: { ...mockInitialAppState } })
        ]
      });
      store = TestBed.get<Store<fromApp.AppState>>(Store);
    });

    it('should have \'editMode = true\' and form values should be set to name and amount of the selected ingredient',
      fakeAsync(() => {
        const fixture = TestBed.createComponent(ShoppingEditComponent);
        const componentInstance = fixture.componentInstance;

        tick();
        fixture.detectChanges();

        fixture.whenStable().then(() => {

          store.setState({
            ...mockInitialAppState,
            shoppingList: {
              // the updated state
              ...shoppingListState,
            }
          });

        });

        tick();
        fixture.detectChanges();

        expect(componentInstance['editMode']).toEqual(true);
      })
    );
  });

Подводя итог, ошибки, которые я делал, были :

  • Неправильная передача в начальное состояние хранилищу
    Вместо передачи типа хранилища от shoppingList до provideMockStore, я передавал тип хранилища ingredients, editedIngredient и editedIngredientIndex

  • Некорректное определение типа магазина
    Вместо того, чтобы определять мой макет магазина с типом AppState, я только определял его как имеющий type ShoppingListState.
    Это означало несоответствие типов между хранилищем в моем тесте и хранилищем в моем компоненте.

В результате обновления магазина, полученные компонентом во время теста, были undefined.

...