在单元测试中使用参数模拟 ngrx 存储选择器(Angular)

Posted

技术标签:

【中文标题】在单元测试中使用参数模拟 ngrx 存储选择器(Angular)【英文标题】:Mock ngrx store selectors with parameters in unit tests (Angular) 【发布时间】:2019-09-08 06:50:33 【问题描述】:

我正在尝试为 Angular 中的服务编写单元测试。 我想模拟 ngrx 的 store.select 函数,所以我可以测试一个服务如何对商店选择器返回的不同值做出反应。我希望能够单独模拟每个选择器。

我的主要问题是如何模拟参数化选择器。

我之前使用了一个映射到 select 函数的 BehaviourSubject,但这不允许您为不同的选择器返回不同的值。它不可读,因为您在模拟什么选择器并不明显。

选项 1:使用主题的模拟存储:无法知道主题对应于哪个选择器,无法为不同的选择器返回不同的值。

// service.spec.ts

const selectSubject = new BehaviourSubject(null);
class MockStore 
    select = () => selectSubject;

opt 2:模拟存储使用开关:适用于不同的选择器,但在选择器具有参数时无法使其工作。

// service.spec.ts

    // This works well but how can I make it work with selectors with parameters??
    const firstSubject = new BehaviourSubject(null);
    const secondSubject = new BehaviourSubject(null);
    class MockStore 
        select = (selector) => 
            switch (selector): 
                case FirstSelector: 
                    return firstSubject;
                
                case SecondSelector: 
                    return secondSubject;
                
             
        ;
    

    describe('TestService', () => 
        let service: TestService;
        let store: Store<any>;

        beforeEach(() => 
            TestBed.configureTestingModule(
              providers: [
                TestService,
                 provide: Store, useClass: MockStore 
              ],
            );

            service = TestBed.get(TestService);
            store = TestBed.get(Store);
          );

          it('should do X when first selector returns A and second selector returns B', () => 
            firstSelectorSubject.next(A);
            secondSelectorSubject.next(B);

        // Write expectation
          );
    );

我想模拟带有参数化选择器的服务方法,所以我可以用不同的 id 值测试 getUserName

getUserName(id: string): Observable<string> 
    return this.store.select(getUser(id)).pipe(
      filter(user => user !== null),
      map(user => user.fullName)
    );
  

【问题讨论】:

您可以将商店包装在服务中,如本文所述***.com/questions/49288024/… 谢谢,但我不认为将选择器包装在服务中以便我可以测试它们是最好的方法。在我看来,Ngrx 已经提供了必要的抽象。 【参考方案1】:

@Tobias Lindgren 提供的解决方案对我有用。扩展他的解决方案,为在这里寻找 Jasmine 的人提供示例:

// spec file -> I have used this snippet in `beforeEach` block
spyOn(Selectors, 'getContent').and.returnValue(
  createSelector(
    // (v: any) => v, <- this is required if you're on NgRx 12, for 13 you can omit this line
    () => mockValue
  )
);

// this is how my selector looked like & I am using NgRx v13
const getContent = (props: string) => createSelector(
  someRootSelector,
  (stateFromRoot) => 
    return 
      // some new configuration by combining stateFromRoot & props
    
   
);

export const Selectors = 
  getContent,
  many other selectors
;

【讨论】:

【参考方案2】:

为什么 overrideSelector 不起作用

@ngrx/store/testing 中的 store 方法 overrideSelector 适用于没有参数的选择器,但不适用于模拟像这样的参数化/工厂选择器:

const getItem = (itemId) => createSelector(
  getItems,
  (items) => items[itemId]
);

每次调用工厂函数都会创建一个新函数,因此测试类和真实类将创建两个单独的函数,因此overrideSelector 将无法匹配函数调用。

使用间谍方法

要模拟工厂选择器,我们可以在 jestjasmine 等测试框架中使用 spy 方法。

开玩笑的代码示例:

import * as ItemSelectors from '../selectors/item.selectors';

...

const mockItem =  someProperty: 1 ;

jest.spyOn(ItemSelectors, 'getItem').mockReturnValue(
  createSelector(
    (v) => v,
    () => mockItem
  )
);

对于 Jasmine,相应的间谍调用将类似于:

  spyOn(ItemSelectors, 'getItem').and.returnValue(...);

记忆工厂功能

另一种方法是记忆工厂函数(即getItem),以便始终为相同的输入参数返回相同的函数(例如,通过在lodash 中使用memoize)。然后就可以使用overrideSelector。但是,请注意,这会构建一个缓存,该缓存会在每次调用 getItem 时继续增长,这可能会导致与内存相关的性能问题。

【讨论】:

【参考方案3】:

我已经解决了一段时间的类似问题,并且认为我已经找到了一种解决方法。

用选择器

export const getItemsByProperty = (property: string, value: any) => createSelector(getAllItems, (items: ItemObj[]) => items.filter((item) => item[property] == value));

在哪里

export const getAllItems = createSelector(getState, (state) => selectAll(state.items));

在我的组件单元测试文件中,我用数据覆盖了 getItemsByProperty 的底层选择器调用 getAllItems 的选择器,然后在我的测试中期待过滤后的数据。如果您要返回的内容发生变化,则只需更新 getAllItems 的结果即可。

【讨论】:

这很有帮助,谢谢。我试图在没有运气的情况下覆盖关闭的选择器。但这是有道理的。【参考方案4】:

NgRx 7.0 包含用于模拟 Store 的 @ngrx/store/testing。有一个非常方便的 overrideSelector 方法。您基本上是在模拟选择器的输出,因此参数无关紧要。

https://medium.com/ngconf/mockstore-in-ngrx-v7-0-f7803708de4e

mockStore.overrideSelector(yourSelector, expectedOutput);

你也可以在MockStore的初始化中设置选择器:

const mockStore = new MockStore<fromState.IState>(
    new MockState(),
    new ActionsSubject(),
    null,
    null,
    [
        selector: yourSelector,
        value: expectedOutput
    ]
);

如果你想实际测试选择器,你应该有一个专门用于选择器的测试文件。要测试参数化选择器,您将使用投影仪方法。它接受选择器作用的状态(或对象)切片和任何参数。在我的示例中,我正在测试 NgRx 实体。

选择器.ts:

export const getEntityById: any = createSelector(
    getEntitiesAsDictionary,
    (entities, props) => entities[props.id]
);

spec.ts:

const expectedId = 111;
const expected =  id: expectedId , user:  userId: expectedId  ;

const iEntities =  
    [expectedId ]: expected, 
    [222]:  id: 222, user:  userId: 222 , 
    [333]:  id: 333, user:  userId:333 
;

const actual = fromState.getEntityById.projector(iEntities,  id: expectedId );

expect(actual).toEqual(expected);

【讨论】:

以上是关于在单元测试中使用参数模拟 ngrx 存储选择器(Angular)的主要内容,如果未能解决你的问题,请参考以下文章

在 Karma 单元测试中模拟导入选择器的值

单元测试:为了测试值随时间的变化而返回可观察数据以返回主题的模拟服务导致TS抛出TS2339

Ngrx - 选择器在存储更改后没有发出新值

NGRX:如何从其他选择器中调用工厂选择器

单元测试茉莉花@NGRX/DATA

Angular 7、Ngrx、Rxjs 6 - 访问延迟加载模块之间的状态