Rxjs 延迟测试NGRX效应

Rxjs 延迟测试NGRX效应,rxjs,ngrx,ngrx-effects,jasmine-marbles,rxjs-marbles,Rxjs,Ngrx,Ngrx Effects,Jasmine Marbles,Rxjs Marbles,我想测试一种效果,其工作原理如下: const expected = cold('------b ', { b: outcome }); 如果已调度LoadEntriesSucces操作,则效果开始 它等待5秒钟 5秒后,http请求被发送 当响应到达时,将调度新操作(取决于响应是成功还是错误) 效果的代码如下所示: @Effect() continuePollingEntries$ = this.actions$.pipe( ofType(SubnetBrowserApiAc

我想测试一种效果,其工作原理如下:

const expected = cold('------b ', { b: outcome });
  • 如果已调度LoadEntriesSucces操作,则效果开始
  • 它等待5秒钟
  • 5秒后,http请求被发送
  • 当响应到达时,将调度新操作(取决于响应是成功还是错误)
  • 效果的代码如下所示:

      @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 }));
            }),
          );
        }),
      );
    
    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 }) }).
    
    我想测试的是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);
    });
    
    然而,这个测试对我不起作用。测试的输出如下所示:

      @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 }));
            }),
          );
        }),
      );
    
    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 }) }).
    

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

    您可以使用jasmine发出的
    done
    回调

    it('should dispatch action after 5 seconds', (done) => {
      const resMock = 'resMock';
      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);
      effects.continuePollingEntries$.subscribe((res)=>{
        expect(res).toEqual(resMock);
        done()
      })
    });
    

    第二种符号不适用于茉莉花大理石,请使用破折号:

     const expected = cold('------b ', { b: completion });
    

    你需要做三件事

    1-在每个之前的
    中,您需要覆盖RxJs的内部调度程序,如下所示:

        import { async } from 'rxjs/internal/scheduler/async';
        import { cold, hot, getTestScheduler } from 'jasmine-marbles';
        beforeEach(() => {.....
            const testScheduler = getTestScheduler();
            async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
    })
    
    2-将延迟替换为延迟,如下所示:
    delayWhen(x=>(真?间隔(50):of(未定义))

    3-使用帧,我真的不知道如何使用秒,所以我使用帧。每帧10毫秒。例如,我上面的延迟是50毫秒,我的帧是-b,这是预期的10毫秒+我还需要50毫秒,这等于额外的5帧,这是------b,如下所示:

    const expected = cold('------b ', { b: outcome });
    

    正如在另一个答案中提到的,测试这种效果的一种方法是使用,但可以用一种更简单的方法

    通过使用TestScheduler虚拟化时间,我们可以同步测试异步RxJS代码。ASCII大理石图为我们提供了一种直观的方式来表示可观察对象的行为。我们可以使用它们来断言一个特定的可观察到的行为符合预期,也可以创建热的和冷的可观察到的,我们可以使用它们作为模拟

    例如,让我们对以下效果进行单元测试:

    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秒,并调用一个服务,然后该服务将分派一个成功或错误操作。用于单元测试该影响的代码如下所示:

    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);
        });
      });
    });
    
    
    从“@angular/core/testing”导入{TestBed};
    从“@ngrx/effects/testing”导入{provideMockActions}”;
    从“rxjs”导入{observeable};
    从“rxjs/testing”导入{TestScheduler};
    从“/foo.effects”导入{FooEffects}”;
    从“./services/foo.service”导入{FooService};
    从“./actions/foo.actions”导入*作为从fooactions导入;
    // ...
    描述(“食物效应”,()=>{
    让动作$:可观察;
    让testScheduler:testScheduler;//{
    //初始化将函数传递给的TestScheduler实例
    //比较两个对象是否相等
    testScheduler=新的testScheduler((实际、预期)=>{
    预期(实际)。toEqual(预期);
    });
    TestBed.configureTestingModule({
    进口:[],
    供应商:[
    食物效应,
    provideMockActions(()=>actions$),
    //模拟服务,以便我们可以测试它是否被调用
    //如果发送了正确的数据
    {
    提供:餐饮服务,
    useValue:jasmine.createSpyObj(“FooService”{
    someMethod:jasmine.createSpy(),
    }),
    },
    ],
    });
    effects=TestBed.inject(FooEffects);
    fooServiceMock=TestBed.inject(FooService);
    });
    描述(“effectWithDelay$”,()=>{
    它(“如果成功,应在5秒后发送doSomethingSuccess”,()=>{
    const someDataMock={someData:Math.random()*100};
    const initialAction=fromFooActions.doSomething(someDataMock);
    const expectedAction=fromFooActions.doSomethingSuccess();
    testScheduler.run((helpers)=>{
    //执行此回调中的代码时,任何运算符
    //使用定时器/异步调度器(如延迟、去BounceTime等)的
    //**自动**改用TestScheduler,以便
    //“虚拟时间”。您不需要将TestScheduler传递给他们,
    //就像过去一样。
    // https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
    常量{热、冷、可观察}=助手;
    //行动/-a-
    //服务//-b|
    //结果//5s--c
    //行动
    actions$=hot(“-a-”,{a:initialAction});
    //服务
    fooServiceMock.someMethod.and.returnValue(cold(“-b |”),{b:null});
    //结果
    预期可观察(effects.effectWithDelay$).toBe(“5s--c”{
    c:预期行动,
    });
    });
    //这需要在run()回调之外
    //因为它是同步执行的:O
    expect(fooServiceMock.someMethod).toHaveBeenCalled();
    期望(fooServiceMock.someMethod).tohavebeincalledtimes(1);
    期望(fooServiceMock.someMethod).tohavencalledwith(someDataMock.someData);
    });
    });
    });
    

    请注意,在代码中,我使用
    expectObservable
    使用TestScheduler实例中的“虚拟时间”来测试效果。

    它根本不测试响应是否在5秒后出现:(你想测试你的效果延迟时间吗?好吧,我猜我想测试的是整个可观察流是否和我期望的一样。在你的例子中,从来没有测试过这个流是否和我期望的一样(参见“期望”变量)当然,我在本例中的意思是,您将提供一个模拟对象和您的响应:)您找到修复程序了吗?我已经准备好使用TestScheduler,但从未让它工作。没有-我最终根本没有测试它:<在我看来,jasmine marbles不再维护,而且有些东西根本不工作(如时间调度器)请参见下面我的回答。它对我有效。您还可以在效果中添加一个参数以禁用计时器,这样您就不必担心通过秒延迟。如果需要帮助,请告诉我。marbels as的问题是,当效果被延迟操作符延迟时,结果将为空。您将