NgRx 测试 - 订阅回调在测试期间不更新

NgRx Testing - Subscribe callback not updating during test

我正在尝试测试编辑购物清单项目的组件。
首次加载时,它通过订阅 store.select 接收到的相关状态值是:

editedIngredient: null,
editedIngredientIndex: -1

使用这些值,它将确保 class' editMode 属性 设置为 false.
在测试期间,我试图在组件加载后更新状态。
我想要实现的是将 editedIngredienteditedIngredientIndex 属性更新为组件中的真值,从而允许 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();
    });
  })
);

供参考: 我从 the NgRx docs 找到了模拟商店的例子(我在 Angular 8,所以这个例子对我来说最相关)

我正在使用 Karma/Jasmine 进行测试。

任何指导都会很有帮助。

经过一些研究,我想我找到了问题所在。

我们来看看provideMockStore's implementation:

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 },
  ];
}

可以给provideMockStoreconfig对象是这样的形状:

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_STATEMockState 只是一个 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,您可以查看 this article

感谢 Andrei 的投入,我设法让 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,而是传递了 ingredientseditedIngredienteditedIngredientIndex

    的商店类型
  • 商店类型定义不正确
    我没有使用 AppState 类型定义模拟商店,而是将其定义为 ShoppingListState.
    类型 这意味着我的测试中的商店和我的组件中的商店之间存在类型不匹配。

这导致组件在测试期间收到的存储更新为 undefined