rxjs - 延迟测试 NGRX 效果

标签 rxjs ngrx ngrx-effects jasmine-marbles rxjs-marbles

我想测试一个效果如下:

  • 如果调度了 LoadEntriesSucces 操作,则效果开始
  • 等待 5 秒
  • 5 秒后发送 http 请求
  • 当响应到达时,将分派(dispatch)新的操作(取决于响应是成功还是错误)。

  • Effect 的代码如下所示:
      @Effect()
      continuePollingEntries$ = this.actions$.pipe(
        ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
        delay(5000),
        switchMap(() => {
          return this.subnetBrowserService.getSubnetEntries().pipe(
            map((entries) => {
              return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
            }),
            catchError((error) => {
              return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
            }),
          );
        }),
      );
    

    我要测试的是效果是否在 5 秒后发送:
    it('should dispatch action after 5 seconds', () => {
      const entries: SubnetEntry[] = [{
        type: 'type',
        userText: 'userText',
        ipAddress: '0.0.0.0'
      }];
    
      const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
      const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
    
      actions$ = hot('-a', { a: action });
      const response = cold('-a', {a: entries});
      const expected = cold('- 5s b ', { b: completion });
    
      subnetBrowserService.getSubnetEntries = () => (response);
    
      expect(effects.continuePollingEntries$).toBeObservable(expected);
    });
    

    但是这个测试对我不起作用。测试的输出如下所示:
    Expected $.length = 0 to equal 3.
    Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
    Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
    Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).
    

    我应该怎么做才能使这个测试工作?

    最佳答案

    就像另一个答案中提到的那样,测试这种效果的一种方法是使用 TestScheduler 但它可以以更简单的方式完成。

    We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.


    例如,让我们对以下效果进行单元测试:
    effectWithDelay$ = createEffect(() => {
      return this.actions$.pipe(
        ofType(fromFooActions.doSomething),
        delay(5000),
        switchMap(({ payload }) => {
          const { someData } = payload;
    
          return this.fooService.someMethod(someData).pipe(
            map(() => {
              return fromFooActions.doSomethingSuccess();
            }),
            catchError(() => {
              return of(fromFooActions.doSomethinfError());
            }),
          );
        }),
      );
    });
    
    效果只是在初始操作后等待 5 秒,然后调用一个服务,然后该服务将分派(dispatch)一个成功或错误操作。对该效果进行单元测试的代码如下:
    import { TestBed } from "@angular/core/testing";
    
    import { provideMockActions } from "@ngrx/effects/testing";
    
    import { Observable } from "rxjs";
    import { TestScheduler } from "rxjs/testing";
    
    import { FooEffects } from "./foo.effects";
    import { FooService } from "../services/foo.service";
    import * as fromFooActions from "../actions/foo.actions";
    
    // ...
    
    describe("FooEffects", () => {
      let actions$: Observable<unknown>;
    
      let testScheduler: TestScheduler; // <-- instance of the test scheduler
    
      let effects: FooEffects;
      let fooServiceMock: jasmine.SpyObj<FooService>;
    
      beforeEach(() => {
        // Initialize the TestScheduler instance passing a function to
        // compare if two objects are equal
        testScheduler = new TestScheduler((actual, expected) => {
          expect(actual).toEqual(expected);
        });
    
        TestBed.configureTestingModule({
          imports: [],
          providers: [
            FooEffects,
            provideMockActions(() => actions$),
    
            // Mock the service so that we can test if it was called
            // and if the right data was sent
            {
              provide: FooService,
              useValue: jasmine.createSpyObj("FooService", {
                someMethod: jasmine.createSpy(),
              }),
            },
          ],
        });
    
        effects = TestBed.inject(FooEffects);
        fooServiceMock = TestBed.inject(FooService);
      });
    
      describe("effectWithDelay$", () => {
        it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
          const someDataMock = { someData: Math.random() * 100 };
    
          const initialAction = fromFooActions.doSomething(someDataMock);
          const expectedAction = fromFooActions.doSomethingSuccess();
        
          testScheduler.run((helpers) => {
    
            // When the code inside this callback is being executed, any operator 
            // that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
            // **automatically** use the TestScheduler instead, so that we have 
            // "virtual time". You do not need to pass the TestScheduler to them, 
            // like in the past.
            // https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
    
            const { hot, cold, expectObservable } = helpers;
    
            // Actions // -a-
            // Service //    -b|
            // Results // 5s --c
    
            // Actions
            actions$ = hot("-a-", { a: initialAction });
    
            // Service
            fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));
    
            // Results
            expectObservable(effects.effectWithDelay$).toBe("5s --c", {
              c: expectedAction,
            });
          });
    
          // This needs to be outside of the run() callback
          // since it's executed synchronously :O
          expect(fooServiceMock.someMethod).toHaveBeenCalled();
          expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
          expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
        });
      });
    });
    
    
    请注意,在我使用的代码中 expectObservable使用 TestScheduler 实例中的“虚拟时间”来测试效果。

    关于rxjs - 延迟测试 NGRX 效果,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54423862/

    相关文章:

    javascript - next() 到中间 Observable

    angular - 使用 Nrwl 的 Nx 中的数据持久化模块,悲观更新的实现与乐观更新有何不同

    ngrx - 为什么在这个@ngrx 示例中需要重新选择 createSelector?

    angular - 操作必须具有类型属性错误 NgRx

    javascript - 创建一个 RXJS 中断定时器

    javascript - RxJS Observable 在一些异步操作后触发 onCompleted

    javascript - 如何过滤 redux store 的 RxJS observables?

    angular - 为什么在导入商店功能后调度操作时我的 reducer 会被多次调用?

    angular - 何时在 angular2 中使用 ngrx/effect