How to test timeout() in a rxjs pipe with jasmine-marbles
Asked Answered
M

1

5

I have written a pipe that filters an input observable. In the pipe I specify a timeout with the timeout() operator to abort waiting if the expected value is not emitted by the source in time. I want to test the timeout case with jasmine-marbles, but I can't get it to work. I believe that expect(source).toBeObservable() evaluates before the source emits.

see Stackblitz

The pipe to be tested:

source = cold('a', { a: { id: 'a' } }).pipe(
  timeout(500),
  filter((a) => false),
  catchError((err) => {
    return of({ timeout: true })
  }),
  take(1)
);

Testing with toPromise() works as expected:

expect(await source.toPromise()).toEqual({ timeout: true });

Testing with jasmine-marbles

const expected = cold('500ms (a|)', { a: { timeout: true } });
expect(source).toBeObservable(expected);

fails with the error

Expected $.length = 0 to equal 2.
Expected $[0] = undefined to equal Object({ frame: 500, notification: Notification({ kind: 'N', value: Object({ timeout: true }), error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 500, notification: Notification({ kind: 'C', value: undefined, error: undefined, hasValue: false }) }).
Mellon answered 23/4, 2019 at 21:2 Comment(0)
T
8

Support for time progression was recently added (see jasmine-marbles PR #38) to jasmine-marbles 0.5.0. Additional test specs were added to the package that demonstrate one of a couple of possible ways to accomplish what you want. Here are some options I was able to throw together using your Stackblitz sample.

Option 1

When you initialize the source observable outside the test method (e.g. in beforeEach), you must explicitly initialize and pass the test scheduler to timeout to get expect().toBeObservable() working. However, take note that this change will break the "should work with toPromise" test. (I don't know why, but toPromise() doesn't appear to work with this approach.)

describe('Marble testing with timeout', () => {

  let source;

  beforeEach(() => {
    // You must explicitly init the test scheduler in `beforeEach`.
    initTestScheduler()
    source = cold('a', { a: { id: 'a' } }).pipe(
      // You must explicitly pass the test scheduler.
      timeout(500, getTestScheduler()),
      filter((a) => false),
      catchError(err => {
        return of({ timeout: true })
      }),
      take(1)
    );
  });

  it('should work with toBeObservable', () => {
    const expected = cold('500ms (a|)', { a: { timeout: true } });
    expect(source).toBeObservable(expected);
  });
});

Option 2

You can refactor things slightly and initialize the source observable inside the test method (not in beforeEach). You don't need to explicitly initializes the test scheduler (jasmine-marbles will do it for you before the test method runs), but you still have to pass it to timeout. Note how the createSource function can be used with the test scheduler or the default scheduler (if the scheduler argument is left undefined). This options works with both the "should work with toPromise" test and the "should work with toBeObservable" test.

describe('Marble testing with timeout', () => {

  const createSource = (scheduler = undefined) => {
    return cold('a', { a: { id: 'a' } }).pipe(
      // You must explicitly pass the test scheduler (or undefined to use the default scheduler).
      timeout(500, scheduler),
      filter((a) => false),
      catchError(err => {
        return of({ timeout: true })
      }),
      take(1)
    );
  };

  it('should work with toPromise', async () => {
    const source = createSource();
    expect(await source.toPromise()).toEqual({ timeout: true });
  });

  it('should work with toBeObservable', () => {
    const source = createSource(getTestScheduler());
    const expected = cold('500ms (a|)', { a: { timeout: true } });
    expect(source).toBeObservable(expected);
  });
});

Option 3

Finally, you can skip passing the test scheduler to timeout if you explicitly use the test scheduler's run method, but you must use expectObservable (as opposed to expect().toBeObservable(). It works just fine, but Jasmine will report the warning "SPEC HAS NO EXPECTATIONS".

describe('Marble testing with timeout', () => {

  let source;

  beforeEach(() => {
    source = cold('a', { a: { id: 'a' } }).pipe(
      timeout(500),
      filter((a) => false),
      catchError(err => {
        return of({ timeout: true })
      }),
      take(1)
    );
  });

  it('should work with scheduler and expectObservable', () => {
    const scheduler = getTestScheduler();
    scheduler.run(({ expectObservable }) => {
      expectObservable(source).toBe('500ms (0|)', [{ timeout: true }]);
    });
  });
});
Thermit answered 24/4, 2019 at 5:59 Comment(2)
It looks like time progression is still getting baked into jasmine-marbles. I thought I'd plug rxjs-marbles. Good experiences using it. It supports Jasmine (among others), and they have a great collection of examples for various use cases. github.com/cartant/rxjs-marbles/tree/master/examplesThermit
Thank you @seniorquico, great response! Opposed to the Stackblitz example, I could not get Option 1 to work in my production code, I have no clue why. But I like Option 3 better anyway, because it does not involve handling a scheduler in the production code. Side note: I avoided the Jasmine warning with expect().nothing()Mellon

© 2022 - 2024 — McMap. All rights reserved.