【问题标题】:how to mock ngrx selector in a component如何在组件中模拟ngrx选择器
【发布时间】:2018-08-23 14:08:50
【问题描述】:

在组件中,我们使用 ngrx 选择器来检索状态的不同部分。

public isListLoading$ = this.store.select(fromStore.getLoading);
public users$ = this.store.select(fromStore.getUsers);

fromStore.method 是使用 ngrx createSelector 方法创建的。例如:

export const getState = createFeatureSelector<UsersState>('users');
export const getLoading = createSelector(
  getState,
  (state: UsersState) => state.loading
);

我在模板中使用了这些 observables:

<div class="loader" *ngIf="isLoading$ | async"></div>
<ul class="userList">
    <li class="userItem" *ngFor="let user of $users | async">{{user.name}}</li>
</div>

我想写一个测试,我可以这样做:

store.select.and.returnValue(someSubject)

能够更改主题值并根据这些值测试组件的模板。

事实上,我们很难找到合适的方法来测试它。如何编写我的“andReturn”方法,因为select 方法在我的组件中被调用了两次,两个不同的方法(MemoizedSelector)作为参数?

我们不想使用真正的选择器,所以模拟一个状态然后使用真正的选择器似乎不是一种合适的单元测试方式(测试不会被隔离,而是使用真正的方法来测试组件的行为)。

【问题讨论】:

  • 一个问题,'returnValue'来自茉莉花,对吧?我面临的另一个问题是,当我在 store.select 中使用选择器功能时。茉莉的间谍解决方案不起作用。但是如果我直接使用 store.select('somefield') ,它可以工作。你有类似的问题吗?
  • @ChrisBao 目前我正面临同样的问题。如果你能解决这个问题,你能分享一下吗?特别是对于状态选择器。

标签: angular jasmine ngrx


【解决方案1】:

我遇到了同样的挑战,并通过将我的选择器包装在服务中一劳永逸地解决了这个问题,因此我的组件只是使用服务来获取它们的数据,而不是直接通过存储。我发现这清理了我的代码,使我的测试与实现无关,并使模拟更容易:

mockUserService = {
  get users$() { return of(mockUsers); },
  get otherUserRelatedData$() { return of(otherMockData); }
}

TestBed.configureTestingModule({
  providers: [{ provide: UserService, useValue: mockUserService }]
});

然而,在我这样做之前,我必须解决你的问题。

您的解决方案将取决于您将数据保存在何处。如果您将其保存在constructor 中,例如:

constructor(private store: Store) {
  this.users$ = store.select(getUsers);
}

然后,每次要更改store 返回的值时,您都需要重新创建测试组件。为此,请按照以下方式创建一个函数:

const createComponent = (): MyComponent => {
  fixture = TestBed.createComponent(MyComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();
  return component;
};

然后在您更改商店间谍返回的值后调用它:

describe('test', () => {
  it('should get users from the store', () => {
    const users: User[] = [{username: 'BlackHoleGalaxy'}]; 
    store.select.and.returnValue(of(users));
    const cmp = createComponent();
    // proceed with assertions
  });
});

或者,如果您在ngOnInit 中设置值:

constructor(private store: Store) {}
ngOnInit() {
  this.users$ = this.store.select(getUsers);
}

事情要简单一些,因为您可以创建一次组件,然后每次想要更改商店的返回值时调用ngOnInit

describe('test', () => {
  it('should get users from the store', () => {
    const users: User[] = [{username: 'BlackHoleGalaxy'}]; 
    store.select.and.returnValue(of(users));
    component.ngOnInit();
    // proceed with assertions
  });
});

【讨论】:

  • 谢谢。仅仅为了测试目的而包装服务是没有意义的。如果您的组件中有多个select,则您的第二个解决方案效果很好。然后store.select.and.returnValue 变得不可用,因为您的两个选择将获得相同的值(这可能会导致应用程序在测试时崩溃,因为异步管道接收并处理对该位置无效的数据。
  • 我同意将它包装在服务中只是进行测试是无稽之谈,但您可能会发现不依赖直接注入商店还有其他好处。此外,如果您将测试设置为一次只测试一件事,则不必担心在同一个单元测试中测试多个值。话虽如此,服务方法确实允许您一次测试多个,因为每个选择器都有一个 getter 或方法。
  • 为什么要使用 ngrx,如果你打算用抽象的方式来包装它,而不是 ngrx 的整个 CQRS 模式。还不如不使用它。
  • 这样你就不能模拟不同的选择器到不同的响应,一次只能模拟一个。这只会涵盖很少的用例。
【解决方案2】:

我也遇到了这个问题,使用服务来包装选择器对我来说也是没有选择的。尤其是不仅出于测试目的,而且因为我使用商店来替换服务。

因此我想出了以下(也不是完美的)解决方案:

我为每个组件和每个不同方面使用不同的“存储”。在您的示例中,我将定义以下 Stores&States:

export class UserStore extends Store<UserState> {}

export class LoadingStatusStore extends Store<LoadingStatusState> {}

并将它们注入到用户组件中:

constructor( private userStore: UserStore, private LoadingStatusStore: 
LoadingStatusStore ) {}

在 User-Component-Test-Class 中模拟它们:

TestBed.configureTestingModule({
  imports: [...],
  declarations: [...],
  providers: [
    { provide: UserStore, useClass: MockStore },
    { provide: LoadingStatusStore, useClass: MockStore }
  ]
}).compileComponents();

将它们注入到 beforeEach() 或 it() 测试方法中:

beforeEach(
  inject(
    [UserStore, LoadingStatusStore],
      (
        userStore: MockStore<UserState>,
        loadingStatusStore: MockStore<LoadingStatusState>
      ) => {...}

然后您可以使用它们来监视不同的管道方法:

const userPipeSpy = spyOn(userStore, 'pipe').and.returnValue(of(user));
const loadingStatusPipeSpy = spyOn(loadingStatusStore, 'pipe')
  .and.returnValue(of(false));

这种方法的缺点是您仍然不能在一个测试方法中测试商店状态的多个部分。但现在这对我来说是一种解决方法。

【讨论】:

    【解决方案3】:

    你可以使用类似的东西:

    spyOn(store, 'select').and.callFake(selectFake);
    
    function pipeFake(op1: OperatorFunction<UsersState, any>): Observable<any> {
      if (op1.toString() === fromStore.getLoading.toString()) {
        return of(true);
      }
    
      if (op1.toString() === fromStore.getUsers.toString()) {
        return of(fakeUsers);
      }
    
      return of({});
    }
    

    【讨论】:

      【解决方案4】:

      我创建了一个这样的助手:

      class MockStore {
              constructor(public selectors: any[]) {
              }
      
              select(calledSelector) {
                const filteredSelectors = this.selectors.filter(s => s.selector === calledSelector);
                if (filteredSelectors.length < 1) {
                  throw new Error('Some selector has not been mocked');
                }
                return cold('a', {a: filteredSelectors[0].value});
              }
       }
      

      现在我的测试看起来像这样:

        const mock = new MockStore([
          {
            selector: selectEditMode,
            value: initialState.editMode
          },
          {
            selector: selectLoading,
            value: initialState.isLoading
          }
        ]);
      
        it('should be initialized', function () {
          const store = jasmine.createSpyObj('store', ['dispatch', 'select']);
          store.select.and.callFake(selector => mock.select(selector));
      
          const comp = new MyComponent(store);
      
          comp.ngOnInit();
      
          expect(comp.editMode$).toBeObservable(cold('a', {a: false}));
          expect(comp.isLoading$).toBeObservable(cold('a', {a: false}));
        });
      

      【讨论】:

      • 这不适用于带参数的选择器,对吗?
      【解决方案5】:

      如果您要自己测试选择器,将选择器移动到服务中并不会消除模拟选择器的需要。 ngrx 现在有自己的模拟方式,如下所述: https://ngrx.io/guide/store/testing

      【讨论】:

      • 这应该是公认的答案,它运行良好,来自官方文档
      【解决方案6】:

      我发现的最佳解决方案是使用 switch 语句为每个选择器返回所需的数据。 solution @vince 仅在模拟一个选择请求时提供。

      例如:

      jest.spyOn(store, "select").mockImplementation((selector) => {
        switch (selector) {
          case selectSelectedKey:
            return of(key);
          case selectCountry:
            return of(CANADA_MOCK);
        }
        return EMPTY;
      });
      

      【讨论】:

        【解决方案7】:

        如果您想要完成的是模拟状态更新,以便您对选择器的订阅接收到一个新值,那么您应该使用 NgRx 在此处建议的内容。 https://ngrx.io/guide/store/testing#using-mock-selectors

        【讨论】:

        • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
        猜你喜欢
        • 1970-01-01
        • 2023-02-04
        • 2017-08-30
        • 2019-09-08
        • 2022-01-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多