如何测试使用商店选择器的 NGRX 效果?
How to test an NGRX effect that uses a store selector?
我有一个 @Effect
,它使用 MemoizedSelector
从 redux 存储中获取一个项目,并 mergeMap
它带有一个 Action 的负载。效果很好,但是为此设置 Jest 测试已证明很困难,因为我似乎无法模拟 select 的 return 值,或者由于 select
是声明的函数导入(来自 '@ngrx/store')并在效果中使用,selector 本身也是一个导入函数。我现在正在抓救命稻草。
如何编写单元测试来测试使用商店的 NGRX 效果 selector?
"@ngrx/store": "^7.4.0",
"rxjs": "^6.2.2"
我试过以下几种解决方法:
- 使用
provideMockStore({
initialState
})
provideMockStore 来自 '@ngrx/store/testing';
,其中初始状态是我的 actual initialState 和包含确切 structure/item 我正在尝试 select
使用来自各种 SO questions/answers 的不同类型的 MockStore
以及不同的博客文章方法
- Mock ngrx store selectors in unit tests (Angular)
- https://blog.angularindepth.com/how-i-test-my-ngrx-selectors-c50b1dc556bc
- https://christianlydemann.com/the-complete-guide-to-ngrx-testing/
- https://medium.com/@adrianfaciu/testing-ngrx-effects-3682cb5d760e
试图模拟 select 或使用 <selector>.projector(<my-mock-object>)
(这里是吸管,我很确定这将用于 [=53= 的隔离测试]select或不是效果)
效果本身:
@Effect()
getReviewsSuccess$ = this.actions$.pipe(
ofType<ProductActions.GetReviewsSuccess>(
ProductActions.ProductActionTypes.GET_REVIEWS_SUCCESS
),
mergeMap(() => this.reduxStore.pipe(select(selectProduct))),
map(({ product_id }) => product_id),
map(product_id => new ProductActions.GetReviewsMeta({
product_id,
}))
);
规范:
......
let effects: ProductEffects;
let facade: Facade;
let actions$: Observable<any>;
let store$: Observable<State>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
// ^ I've also tried using StoreModule.forRoot(...) here to configure
// it in similar fashion to the module where this effect lives
],
providers: [
ProductEffects,
provideMockActions(() => actions$),
{
provide: Facade,
useValue: facadeServiceMock,
},
ResponseService,
provideMockStore({
initialState
})
// ^ also tried setting up the test with different variations of initialState
],
});
......
it('should return a GetReviewsMeta on successful GetReviewsSuccess', () => {
const reviews = {...reviewListMock};
const { product_id } = {...productMockFull};
const action = new ProductActions.GetReviewsSuccess({
reviews
});
const outcome = new ProductActions.GetReviewsMeta({
product_id
});
actions$ = hot('-a', { a: action });
// store$ = cold('-c', { c: product_id });
// not sure what, if anything I need to do here to mock select(selectProduct)
const expected = cold('-b', { b: outcome });
expect(effects.getReviewsSuccess$).toBeObservable(expected);
});
选择器selectProduct
:
export const getProduct = ({product}: fromProducts.State) => product;
export const getProductState = createFeatureSelector<
fromProducts.State
>('product');
export const selectProduct = createSelector(
getProductState,
getProduct,
);
我希望测试能够通过,但我却不断收到以下错误
● Product Effects › should return a GetReviewsMeta on successful GetReviewsSuccess
expect(received).toBeNotifications(expected)
Expected notifications to be:
[{"frame": 10, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": {"product_id": 2521}, "type": "[Reviews] Get Reviews Meta"}}}]
But got:
[{"frame": 10, "notification": {"error": [TypeError: Cannot read property 'product_id' of undefined], "hasValue": false, "kind": "E", "value": undefined}}]
显然 MemoizedSelector
(selectProduct) 不知道应该在商店中的产品对象是什么(但似乎不知道我是否注入 initialState
有或没有)并且无法获得产品的 product_id
因为我没有在 beforeEach
或规范本身中正确设置它...
我们在 ngrx.io 文档中对此进行了介绍。请注意,语法适用于 NgRx 8,但同样的想法也适用于 NgRx 7。
addBookToCollectionSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(CollectionApiActions.addBookSuccess),
withLatestFrom(this.store.pipe(select(fromBooks.getCollectionBookIds))),
tap(([, bookCollection]) => {
if (bookCollection.length === 1) {
window.alert('Congrats on adding your first book!');
} else {
window.alert('You have added book number ' + bookCollection.length);
}
})
),
{ dispatch: false }
);
it('should alert number of books after adding the second book', () => {
store.setState({
books: {
collection: {
loaded: true,
loading: false,
ids: ['1', '2'],
},
},
} as fromBooks.State);
const action = CollectionApiActions.addBookSuccess({ book: book1 });
const expected = cold('-c', { c: action });
actions$ = hot('-a', { a: action });
expect(effects.addBookToCollectionSuccess$).toBeObservable(expected);
expect(window.alert).toHaveBeenCalledWith('You have added book number 2');
});
});
确保您的状态与 redux devtools 中的结构相同。
NgRx 8 还提供了一种模拟选择器的方法,因此不需要为单个测试设置整个状态树 - https://next.ngrx.io/guide/store/testing#using-mock-selectors.
describe('Auth Guard', () => {
let guard: AuthGuard;
let store: MockStore<fromAuth.State>;
let loggedIn: MemoizedSelector<fromAuth.State, boolean>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthGuard, provideMockStore()],
});
store = TestBed.get(Store);
guard = TestBed.get(AuthGuard);
loggedIn = store.overrideSelector(fromAuth.getLoggedIn, false);
});
it('should return false if the user state is not logged in', () => {
const expected = cold('(a|)', { a: false });
expect(guard.canActivate()).toBeObservable(expected);
});
it('should return true if the user state is logged in', () => {
const expected = cold('(a|)', { a: true });
loggedIn.setResult(true);
expect(guard.canActivate()).toBeObservable(expected);
});
});
我有一个 @Effect
,它使用 MemoizedSelector
从 redux 存储中获取一个项目,并 mergeMap
它带有一个 Action 的负载。效果很好,但是为此设置 Jest 测试已证明很困难,因为我似乎无法模拟 select 的 return 值,或者由于 select
是声明的函数导入(来自 '@ngrx/store')并在效果中使用,selector 本身也是一个导入函数。我现在正在抓救命稻草。
如何编写单元测试来测试使用商店的 NGRX 效果 selector?
"@ngrx/store": "^7.4.0",
"rxjs": "^6.2.2"
我试过以下几种解决方法:
- 使用
provideMockStore({
initialState
})
provideMockStore 来自 '@ngrx/store/testing';
,其中初始状态是我的 actual initialState 和包含确切 structure/item 我正在尝试 select
使用来自各种 SO questions/answers 的不同类型的
MockStore
以及不同的博客文章方法- Mock ngrx store selectors in unit tests (Angular)
- https://blog.angularindepth.com/how-i-test-my-ngrx-selectors-c50b1dc556bc
- https://christianlydemann.com/the-complete-guide-to-ngrx-testing/
- https://medium.com/@adrianfaciu/testing-ngrx-effects-3682cb5d760e
试图模拟 select 或使用
<selector>.projector(<my-mock-object>)
(这里是吸管,我很确定这将用于 [=53= 的隔离测试]select或不是效果)
效果本身:
@Effect()
getReviewsSuccess$ = this.actions$.pipe(
ofType<ProductActions.GetReviewsSuccess>(
ProductActions.ProductActionTypes.GET_REVIEWS_SUCCESS
),
mergeMap(() => this.reduxStore.pipe(select(selectProduct))),
map(({ product_id }) => product_id),
map(product_id => new ProductActions.GetReviewsMeta({
product_id,
}))
);
规范:
......
let effects: ProductEffects;
let facade: Facade;
let actions$: Observable<any>;
let store$: Observable<State>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
// ^ I've also tried using StoreModule.forRoot(...) here to configure
// it in similar fashion to the module where this effect lives
],
providers: [
ProductEffects,
provideMockActions(() => actions$),
{
provide: Facade,
useValue: facadeServiceMock,
},
ResponseService,
provideMockStore({
initialState
})
// ^ also tried setting up the test with different variations of initialState
],
});
......
it('should return a GetReviewsMeta on successful GetReviewsSuccess', () => {
const reviews = {...reviewListMock};
const { product_id } = {...productMockFull};
const action = new ProductActions.GetReviewsSuccess({
reviews
});
const outcome = new ProductActions.GetReviewsMeta({
product_id
});
actions$ = hot('-a', { a: action });
// store$ = cold('-c', { c: product_id });
// not sure what, if anything I need to do here to mock select(selectProduct)
const expected = cold('-b', { b: outcome });
expect(effects.getReviewsSuccess$).toBeObservable(expected);
});
选择器selectProduct
:
export const getProduct = ({product}: fromProducts.State) => product;
export const getProductState = createFeatureSelector<
fromProducts.State
>('product');
export const selectProduct = createSelector(
getProductState,
getProduct,
);
我希望测试能够通过,但我却不断收到以下错误
● Product Effects › should return a GetReviewsMeta on successful GetReviewsSuccess
expect(received).toBeNotifications(expected)
Expected notifications to be:
[{"frame": 10, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": {"product_id": 2521}, "type": "[Reviews] Get Reviews Meta"}}}]
But got:
[{"frame": 10, "notification": {"error": [TypeError: Cannot read property 'product_id' of undefined], "hasValue": false, "kind": "E", "value": undefined}}]
显然 MemoizedSelector
(selectProduct) 不知道应该在商店中的产品对象是什么(但似乎不知道我是否注入 initialState
有或没有)并且无法获得产品的 product_id
因为我没有在 beforeEach
或规范本身中正确设置它...
我们在 ngrx.io 文档中对此进行了介绍。请注意,语法适用于 NgRx 8,但同样的想法也适用于 NgRx 7。
addBookToCollectionSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(CollectionApiActions.addBookSuccess),
withLatestFrom(this.store.pipe(select(fromBooks.getCollectionBookIds))),
tap(([, bookCollection]) => {
if (bookCollection.length === 1) {
window.alert('Congrats on adding your first book!');
} else {
window.alert('You have added book number ' + bookCollection.length);
}
})
),
{ dispatch: false }
);
it('should alert number of books after adding the second book', () => {
store.setState({
books: {
collection: {
loaded: true,
loading: false,
ids: ['1', '2'],
},
},
} as fromBooks.State);
const action = CollectionApiActions.addBookSuccess({ book: book1 });
const expected = cold('-c', { c: action });
actions$ = hot('-a', { a: action });
expect(effects.addBookToCollectionSuccess$).toBeObservable(expected);
expect(window.alert).toHaveBeenCalledWith('You have added book number 2');
});
});
确保您的状态与 redux devtools 中的结构相同。
NgRx 8 还提供了一种模拟选择器的方法,因此不需要为单个测试设置整个状态树 - https://next.ngrx.io/guide/store/testing#using-mock-selectors.
describe('Auth Guard', () => {
let guard: AuthGuard;
let store: MockStore<fromAuth.State>;
let loggedIn: MemoizedSelector<fromAuth.State, boolean>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthGuard, provideMockStore()],
});
store = TestBed.get(Store);
guard = TestBed.get(AuthGuard);
loggedIn = store.overrideSelector(fromAuth.getLoggedIn, false);
});
it('should return false if the user state is not logged in', () => {
const expected = cold('(a|)', { a: false });
expect(guard.canActivate()).toBeObservable(expected);
});
it('should return true if the user state is logged in', () => {
const expected = cold('(a|)', { a: true });
loggedIn.setResult(true);
expect(guard.canActivate()).toBeObservable(expected);
});
});