【问题标题】:Angular NgRx Effect errors in Marble Testing: Expected $.length = 0 to equal 2. / Expected $[0] = undefined to equal Object大理石测试中的角度 NgRx 效果错误:预期 $.length = 0 等于 2。/预期 $[0] = 未定义等于 Object
【发布时间】:2020-06-11 09:13:27
【问题描述】:

我有一个使用 Angular 和 NgRx 的应用程序,我在使用 Marble 测试测试我的效果时遇到了困难。

我得到的错误是:

Expected $.length = 0 to equal 2.
Expected $[0] = undefined to equal Object({ frame: 10, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'C', value: undefined, error: undefined, hasValue: false }) }).

效果如下:

@Injectable()
export class OrderLogisticStatusEffects {
  loadOrdersLogisticStatus$ = createEffect(() =>
    this.actions$.pipe(
      ofType(LOAD_ORDERS_LOGISTIC_STATUS),
      withLatestFrom(this.store$.pipe(select(orderLogisticsStatusPollingIntervalSelector))),
      switchMap(([action, pollingInterval]) =>
        timer(0, pollingInterval).pipe(
          withLatestFrom(this.store$.pipe(select(selectedCompanySelector))),
          switchMap(([timerNum, company]) => this.loadOrdersLogisticStatus(pollingInterval, company))
        )
      )
    )
  );

  constructor(private actions$: Actions, private orderLogisticStatusService: OrderLogisticStatusService, private store$: Store<AppState>) {}

  private loadOrdersLogisticStatus(
    pollingInterval: number,
    company: Company
  ): Observable<LoadOrderLogisticStatusSuccess | LoadOrderLogisticStatusFail> {
    if (!company?.logisticsToken) {
      return of(new LoadOrderLogisticStatusFail(new Error('No company selected')));
    }

    this.orderLogisticStatusService.getOrdersStatus(company.logisticsToken).pipe(
      timeout(pollingInterval),
      map((result) => new LoadOrderLogisticStatusSuccess(result)),
      catchError((error) => {
        if (error.name === 'TimeoutError') {
          console.warn('Timeout error while loadin logistic status service', error);
        } else {
          console.error('Error loading order logistic status', error);
          Sentry.captureException(error);
        }

        return of(new LoadOrderLogisticStatusFail(error));
      })
    );
  }
}

这是我的测试:

fdescribe('Order Logistic Status Effect', () => {
  let actions$: Observable<Action>;
  let effects: OrderLogisticStatusEffects;

  describe('With a selected company', () => {
    beforeEach(() => {
      const mockState = {
        ordersLogisticStatus: {
          pollingInterval: 10,
        },
        company: {
          selectedCompany: {
            logisticsToken: 'ey.xxxx.yyyy',
          },
        },
      };

      TestBed.configureTestingModule({
        providers: [
          { provide: OrderLogisticStatusService, useValue: jasmine.createSpyObj('orderLogisticsStatusServiceSpy', ['getOrdersStatus']) },
          OrderLogisticStatusEffects,
          provideMockActions(() => actions$),
          provideMockStore({
            selectors: [
              {
                selector: orderLogisticsStatusPollingIntervalSelector,
                value: 30,
              },
              {
                selector: selectedCompanySelector,
                value: {
                  logisticsToken: 'ey.xxxx.yyy',
                },
              },
            ],
          }),
        ],
      });

      effects = TestBed.inject<OrderLogisticStatusEffects>(OrderLogisticStatusEffects);
    });

    it('should sucessfully load the orders logistics status', () => {
      const service: jasmine.SpyObj<OrderLogisticStatusService> = TestBed.inject(OrderLogisticStatusService) as any;
      service.getOrdersStatus.and.returnValue(cold('-a|', { a: mockData }));

      actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
      const expected = hot('-a|', {
        a: new LoadOrderLogisticStatusSuccess(mockData),
      });

      getTestScheduler().flush();
      expect(effects.loadOrdersLogisticStatus$).toBeObservable(expected);
    });
  });
});

const mockData = {
  1047522: {
    status: 0,
    partner: {
      id: 1,
    },
    eta: '2020-06-09 10:00',
    pickupEta: '2020-06-09 12:00',
  },
};

问题似乎与我的服务模拟有关。即使我将它配置为返回一个冷的 observable,它似乎返回 undefined。

谁能帮帮我?

堆栈闪电战https://stackblitz.com/edit/angular-effects-test

【问题讨论】:

  • 我认为问题在于 actions$ 在您的效果类注入它之后 被初始化,因此它会获得旧值。尝试在effects = TestBed.inject...之前设置actions$ = hot('a',...)
  • 嗨,我不认为是这种情况,因为我的效果被调用了。问题发生在我的服务模拟中。不知何故,模拟函数返回未定义而不是冷观察者。
  • '$.length = 0 to equal 2' 让我觉得 actions$ 根本没有发出。我可能错了。你能创建一个堆栈闪电战吗?
  • 是的,我已经用这个 stackblitz 链接更新了描述:stackblitz.com/edit/angular-effects-test

标签: angular rxjs ngrx


【解决方案1】:

StackBlitz.


有几个问题:

首先,loadOrdersLogisticStatus 缺少返回:

loadOrdersLogisticStatus (/* ... */) {
  return this.orderLogisticStatusService.getOrdersStatus(/* ... */)
}

然后,我发现jasmine-marbles 不会自动设置AsyncScheduler.delegate,而不是TestScheduler.run

 run<T>(callback: (helpers: RunHelpers) => T): T {
  const prevFrameTimeFactor = TestScheduler.frameTimeFactor;
  const prevMaxFrames = this.maxFrames;

  TestScheduler.frameTimeFactor = 1;
  this.maxFrames = Infinity;
  this.runMode = true;
  AsyncScheduler.delegate = this;

  /* ... */
}

这很重要,因为在使用弹珠时,一切都是同步。但是在你的实现中,有一个timer(0, pollingInterval) observable,默认情况下使用AsyncScheduler。如果不设置 AsyncScheduler.delegate,我们将执行异步操作,我认为这是主要问题。

所以,为了设置delegate 属性,我在beforeEach() 中添加了这一行:

AsyncScheduler.delegate = getTestScheduler();

最后,我认为您的断言存在一个小问题。您的效果属性似乎从未完成,并且您还在使用timer(0, pollingInterval)。所以我认为你现在可以做的是添加 take(N) 运算符以测试 N 排放:

it("should sucessfully load the orders logistics status", () => {
  const service: jasmine.SpyObj<OrderLogisticStatusService> = TestBed.inject(OrderLogisticStatusService) as any;
  service.getOrdersStatus.and.callFake(() => cold('-a|', { a: mockData }));

  actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
  const expected = hot('-a--(b|)', {
    a: new LoadOrderLogisticStatusSuccess(mockData),
    b: new LoadOrderLogisticStatusSuccess(mockData),
  });

  expect(effects.loadOrdersLogisticStatus$.pipe(take(2))).toBeObservable(expected);
});

'-a--(b|)' - a 在第 10 帧发送,b + complete 通知(由于take)在40th 帧发送,因为pollingInterval30当第二个通知(b) 被安排时,当前帧将是10

【讨论】:

  • 您好安德烈,感谢您提供的所有帮助。最后,我不得不对您的答案进行一些调整以使一切正常。如果您想更新它,我很乐意将其标记为此问题的答案。
  • 我在下面添加的自定义事件参数:createEffect(() =&gt; ({ scheduler = asyncScheduler, stopTimer = EMPTY } = {}),以及他们对测试的调用。
  • 好的,但那是你的解决方案,不是我的 :)。我很高兴你解决了。但是,我很确定您可以将相同的方法应用于您的实际案例。它不是这样工作的吗? :“AsyncScheduler.delegate = getTestScheduler()”。另外,我想说第二个“问题”是预期的,因为它与你有一个 setInterval(即带有第二个参数的计时器)相同,只是翻转是同步发生的。
  • 我只是想给你更多的帮助,因为如果不是你,我永远不会考虑调查调度程序本身。 ;)
  • 使用这个:AsyncScheduler.delegate = getTestScheduler() 确实在 Stackblitz 中工作,但它在我的实际项目中不起作用。我认为这是因为 AsyncScheduler 在 rxjs 内部很深,并且更改在其他地方被覆盖了。将调度程序直接传递到效果中就是诀窍。
【解决方案2】:

Andrei 的解决方案确实适用于 Stack Blitz,但它不适用于我自己的环境,我认为这是因为一些导入混淆。但其背后的想法是:jasmine-marblesTestScheduler 不会取代 RxJs 的 AsyncScheduler 是正确的。这是我的问题(除了忘记在loadOrdersLogisticStatus 函数中添加returnstatement)。

所以为了让一切正常工作,我必须做两件事:

首先我必须手动将效果的调度程序设置为TestScheduler。当我这样做时,我终于能够看到 timer observable 正在发出结果,而不是像以前那样 undefined

但是我遇到了另一个问题。 timer 不会停止发射,所以我得到了一个像这样的长错误流:

Expected $.length = 26 to equal 2.
Unexpected $[2] = Object({ frame: 60, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[3] = Object({ frame: 90, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[4] = Object({ frame: 120, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[5] = Object({ frame: 150, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[6] = Object({ frame: 180, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[7] = Object({ frame: 210, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[8] = Object({ frame: 240, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[9] = Object({ frame: 270, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.

前两个值可以,但其他值不行。所以我做的第二件事是使用takeUntil 运算符取消订阅timer 一个测试完成(我不确定这是否是最好的方法,因为我绝不能在测试,否则我的投票将停止)。

这是代码的最终工作版本:

效果:

@Injectable()
export class OrderLogisticStatusEffects {
  loadOrdersLogisticStatus$ = createEffect(() => ({ scheduler = asyncScheduler, stopTimer = EMPTY } = {}) =>
    this.actions$.pipe(
      ofType(LOAD_ORDERS_LOGISTIC_STATUS),
      withLatestFrom(this.store$.pipe(select(orderLogisticsStatusPollingIntervalSelector))),
      switchMap(([action, pollingInterval]) =>
        timer(0, pollingInterval, scheduler).pipe(
          takeUntil(stopTimer),
          withLatestFrom(this.store$.pipe(select(selectedCompanySelector))),
          switchMap(([timerNum, company]) => this.loadOrdersLogisticStatus(pollingInterval, company))
        )
      )
    )
  );

  constructor(private actions$: Actions, private orderLogisticStatusService: OrderLogisticStatusService, private store$: Store<AppState>) {}

  private loadOrdersLogisticStatus(
    pollingInterval: number,
    company: Company
  ): Observable<LoadOrderLogisticStatusSuccess | LoadOrderLogisticStatusFail> {
    if (!company?.logisticsToken) {
      return of(new LoadOrderLogisticStatusFail(new Error('No company selected')));
    }

    return this.orderLogisticStatusService.getOrdersStatus(company.logisticsToken).pipe(
      timeout(pollingInterval),
      map((result) => new LoadOrderLogisticStatusSuccess(result)),
      catchError((error) => {
        if (error.name === 'TimeoutError') {
          console.warn('Timeout error while loadin logistic status service', error);
        } else {
          console.error('Error loading order logistic status', error);
          Sentry.captureException(error);
        }

        return of(new LoadOrderLogisticStatusFail(error));
      })
    );
  }
}

测试:

fdescribe('Order Logistic Status Effect', () => {
  let actions$: Observable<Action>;
  let effects: OrderLogisticStatusEffects;

  describe('With a selected company', () => {
    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [
          { provide: OrderLogisticStatusService, useValue: jasmine.createSpyObj('orderLogisticsStatusServiceSpy', ['getOrdersStatus']) },
          OrderLogisticStatusEffects,
          provideMockActions(() => actions$),
          provideMockStore({
            selectors: [
              {
                selector: orderLogisticsStatusPollingIntervalSelector,
                value: 30,
              },
              {
                selector: selectedCompanySelector,
                value: {
                  logisticsToken: 'ey.xxxx.yyy',
                },
              },
            ],
          }),
        ],
      });

      effects = TestBed.inject<OrderLogisticStatusEffects>(OrderLogisticStatusEffects);
    });

    it('should sucessfully load the orders logistics status', () => {
      const service = TestBed.inject(OrderLogisticStatusService) as jasmine.SpyObj<OrderLogisticStatusService>;
      service.getOrdersStatus.and.callFake(() => cold('a|', { a: mockData }));

      actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
      const expected = hot('a--b', {
        a: new LoadOrderLogisticStatusSuccess(mockData),
        b: new LoadOrderLogisticStatusSuccess(mockData),
      });

      const stopTimer = hot('----a', { a: 'stop' });

      const testScheduler = getTestScheduler();
      expect(effects.loadOrdersLogisticStatus$({ scheduler: testScheduler, stopTimer })).toBeObservable(expected);
    });
  });
});

const mockData = {
  1047522: {
    status: 0,
    partner: {
      id: 1,
      slug: 'loggi',
    },
    eta: '2020-06-09 10:00',
    pickupEta: '2020-06-09 12:00',
  },
};

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-27
    • 1970-01-01
    相关资源
    最近更新 更多