Home > Net >  RxJs: Execute a long running function immediately after returning, giving the caller an Observable f
RxJs: Execute a long running function immediately after returning, giving the caller an Observable f

Time:03-19

I have a function that could be considered long running (actually, it's multi-step where each step could be waiting for an external event like a response from a HTTP call).

There's an invoker function to this which should return an observable that returns the updates to the original function. The original function MUST run whether or not the returned observable is subscribed to. Basically, it needs to return a hot observable.

I have tried the following approach but, cannot get it to work:

function longRunningOperation(): Observable<number> {
  const updates$ = new Subject<number>();
  Promise.resolve().then(() => {
    console.log('starting updates...');
    updates$.next(1);
    updates$.next(2);
    updates$.next(3);
    updates$.complete();
  });
  return updates$;
}

If I do a marble test on the above, I see that the actual events generated being empty (though the function does execute).

  it('should return and start executing', () => {
    const updates$ = longRunningOperation();
    const marbles = '(abc|)';
    const events = { a: 1, b: 2, c: 3 };

    new TestScheduler((actual, expected) =>
      expect(actual).toEqual(expected)
    ).run(({ expectObservable }) => {
      expectObservable(updates$).toBe(marbles, events);
      console.log('Test Scheduler subscribed');
    });
  });

What am I doing wrong here?

Link to demo https://stackblitz.com/edit/jasmine-in-angular-upoavr?file=src/app/app.component.spec.ts

CodePudding user response:

I do not think you can test this scenario with marbles, at least in a simple way.

Marbles are a synchronous mechanism, while Promises are always asynchronous.

Therefore, when your test executes the run method of TestScheduler it does it synchronously. The Promise though will be resolved later by the JS engine, so only later the update$ Subject will emit its values.

This is the reason why the test says that it gets 0 notifications from update$ rather than the 4 it expects. The notifications from update$ will come after the assertion has been evaluated.

If you want to test this scenario without marbles, you can do something like this

describe('Testing tests', () => {
  it('should return and start executing', (done) => {
    const updates$ = longRunningOperation();
    const expected = [1, 2, 3]
    const actual = []

    updates$.subscribe({
      next: d => {
        actual.push(d)
      },
      complete: () => {
        // expected equal to actual
        expect(actual.length).toEqual(expected.length)
        actual.forEach((v, i) => expect(v).toEqual(expected[i]))
        done()
      }
    })

  });
});

as can be seen in this stackblitz.

UPDATE

Maybe there is a way to test your scenario with marbles, but this requires a change in the structure of your function.

Marble tests, for what is my experience, define some sort of source streams, apply to such streams some kind of transformations and then compare the results of the transformation with the expected stream values.

In this case the source stream can be a simple Observable that notifies just once. This notification than triggers the execution of the long running function which itself causes update$ to notify.

So, we can change slightly the longRunningOperation function like this

function longRunningOperation_(start$: Observable<any>): Observable<number> {
  const updates$ = new Subject<number>();
  // as soon as start$ notifies, the long running function is executed
  start$.pipe(
    tap(() => {
      console.log('starting updates...');
      updates$.next(1);
      updates$.next(2);
      updates$.next(3);
      updates$.complete();
    })
  )
    .subscribe();

  return updates$;
}

Once you have done so, you have created the space for a source stream (in this case represented by the Observable passed to the longRunningOperation_ function as parameter) and so the test can be run like this

it('should return and start executing', () => {
    new TestScheduler((actual, expected) => {
      console.log('Comparing...');
      console.log('actual:', actual);
      console.log('expected:', expected);
      return expect(actual).toEqual(expected);
    }).run(({ hot, expectObservable }) => {
      const start = hot('0');
      const marbles = '(abc|)';
      const events = { a: 1, b: 2, c: 3 };

      const updates$ = longRunningOperation_(start);
      expectObservable(updates$).toBe(marbles, events);
      console.log('Sheduler subscribed');
    });
  });

Look at this stackblitz for the full example.

  • Related