延迟测试NGRX效果

时间:2019-01-29 15:00:47

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

我要测试一种效果,如下所示:

  1. 如果分派LoadEntriesSucces操作,效果就会开始
  2. 等待5秒
  3. 5秒过后,http请求被发送
  4. 响应到达时,将分派新的动作(取决于响应是成功还是错误)。

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 }) }).

我应该怎么做才能使此测试有效?

4 个答案:

答案 0 :(得分:1)

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

<块引用>

我们可以通过使用 TestScheduler 虚拟化时间来同步和确定性地测试我们的异步 RxJS 代码。 ASCII 弹珠图为我们提供了一种可视化的方式来表示 Observable 的行为。我们可以使用它们来断言特定 Observable 的行为符合预期,以及创建可以用作模拟的热和冷 Observable。

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

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);
    });
  });
});

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

答案 1 :(得分:0)

您可以使用茉莉花中的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()
  })
});

答案 2 :(得分:0)

第二种表示法不适用于jasmine-marbles,请使用破折号代替:

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

答案 3 :(得分:0)

您需要做三件事

1-在beforeEach内,您需要按如下方式覆盖RxJ的内部调度程序:

    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 => (true ? interval(50) : of(undefined)))

3-使用框架,我不太确定该如何使用秒,因此我使用了框架。每帧为10ms。因此,例如,我的延迟时间为50毫秒,我的帧为-b,这是预期的10毫秒+我需要另外50毫秒,所以这等于------ b的5帧,如下所示:

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