NgRx 测试 - 订阅回调在测试期间不更新
NgRx Testing - Subscribe callback not updating during test
我正在尝试测试编辑购物清单项目的组件。
首次加载时,它通过订阅 store.select
接收到的相关状态值是:
editedIngredient: null,
editedIngredientIndex: -1
使用这些值,它将确保 class' 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();
});
})
);
- 我在尝试 1 时遇到的问题是没有进行更改检测。
- 我在尝试 2 时遇到的问题是,如果我省略
done()
回调,则不会进行更改检测。
但是,如果我包含 done()
回调,我会收到一条错误消息,指出 done() is not a function
供参考:
我从 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 },
];
}
可以给provideMockStore
的config
对象是这样的形状:
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
,您可以查看 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
,而是传递了 ingredients
、editedIngredient
和 editedIngredientIndex
的商店类型
商店类型定义不正确
我没有使用 AppState
类型定义模拟商店,而是将其定义为 ShoppingListState
.
类型
这意味着我的测试中的商店和我的组件中的商店之间存在类型不匹配。
这导致组件在测试期间收到的存储更新为 undefined
。
我正在尝试测试编辑购物清单项目的组件。
首次加载时,它通过订阅 store.select
接收到的相关状态值是:
editedIngredient: null,
editedIngredientIndex: -1
使用这些值,它将确保 class' 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();
});
})
);
- 我在尝试 1 时遇到的问题是没有进行更改检测。
- 我在尝试 2 时遇到的问题是,如果我省略
done()
回调,则不会进行更改检测。
但是,如果我包含done()
回调,我会收到一条错误消息,指出done() is not a function
供参考: 我从 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 },
];
}
可以给provideMockStore
的config
对象是这样的形状:
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
,您可以查看 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
,而是传递了ingredients
、editedIngredient
和editedIngredientIndex
商店类型定义不正确
我没有使用AppState
类型定义模拟商店,而是将其定义为ShoppingListState
.
类型 这意味着我的测试中的商店和我的组件中的商店之间存在类型不匹配。
这导致组件在测试期间收到的存储更新为 undefined
。