cartant / rxjs-marbles

An RxJS marble testing library for any test framework

Home Page:https://cartant.github.io/rxjs-marbles/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Dealing with Promises

brentd opened this issue · comments

First, thanks for the lib! I've successfully written several tests, and it's very effective.

I hit a wall recently though when attempting to test an observable that mergeMaps a Promise (in my case, one that waits on a network request, which I've stubbed in the test with nock).

Simplified example:

  it('promises', marbles(m => {
    const obs1 = Observable.from(['a','b','c']).mergeMap(x => Observable.of(x))
    m.equal(obs1, '(abc|)') // Works

    const obs2 = Observable.from(['a','b','c']).mergeMap(x => Promise.resolve(x))
    m.equal(obs2, '(abc|)') // Fails
  }))

The second assertion fails with:

     Error:
Expected

to deep equal
	{"frame":0,"notification":{"kind":"N","value":"a","hasValue":true}}
	{"frame":0,"notification":{"kind":"N","value":"b","hasValue":true}}
	{"frame":0,"notification":{"kind":"N","value":"c","hasValue":true}}
	{"frame":0,"notification":{"kind":"C","hasValue":false}}

I assume this is because Promises (the native implementation, anyway) always resolves on the next tick, while marble testing is intended to test the observable within one tick.

Is my only option to do something hacky like mock the function that returns a promise to return a Observable.of() stubbed value instead?

Yep. You are correct.

The problem is that promises always resolve asynchronously, so your mock is still asynchronous and not compatible with the TestScheduler - which runs the subscriptions in a synchronous manner when its flush method is called (usually at the end of each test). That's why the Expected output is empty - the test has finished before the promise resolved.

You are also correct in that the solution would involve mocking the promise with something the TestScheduler understands and can treat in a synchronous manner (either a synchronous observable - like Observable.of - or an asynchronous observable that takes the TestScheduler instance as a parameter).

Being able to use marble tests with observables that cannot be made to use virtual time is something that I have on my list of problems to solve, but I've not yet devoted any time to it. I have a large number of mocked Firebase tests - written about 12 months ago - that would have been much easier to write using marble tests. Finding a solution is something that will likely wait until I again need to write a substantial number of tests for non-virtual-time observables.

@cartant thanks a ton for the reply. Makes complete sense.

For now, I'll stub the promise APIs using Observable.of, as that sounds pretty straightforward - just a tad messy.

If I get fed up with that, I might fork the project and have a go at that non-virtual-time implementation myself :)

Re: the mock, if you want to include some delay in your mocked promise, you can use the recently added bind method to avoid passing the TestScheduler instance.

For example, if bind is called to ensure the schedulers are bound to the test's TestScheduler instances, you could use Observable.of(response).delay(10) to mock a network request that takes 10 virtual time frames - i.e. one - character in the marble test.

That might be preferable to pretending the network request is synchronous - that is, the marble diagram will include the frames required for the resolution of the network request.

@cartant that's perfect - you're right, pretending the network requests are synchronous will make for some counter intuitive marble tests. Thanks!

Should this be left open to track if there has been progress on this? Or has this been dropped?

At this stage, I have no intention of adding promise-related features to the package.

Ok, I think I was looking more for direction in how asynchronous stuff is handled within the marble callback. In jest it still supports returning promises. So this is exactly what I needed.

const bindMouseEvents = jest.fn(); // injected into module
describe('some epics', () => {
  it(
    viewerMouseEvents.name,
    marbles((m) => {
      const action$ = m.hot('-a-|', {
        a: { type: 'SOME_EVENT', payload: {} }
      });

      // ignore elements strips all elements but errors and completes
      const expected = m.cold('---|');

      const state$ = {};

      const out = viewerMouseEvents(action$, state$);
      m.expect(out).toBeObservable(expected);
      return out
        .toPromise() // don't miss and make sure stuff internally is called
        .then(() => expect(bindMouseEvents).toHaveBeenCalled());
    })
  );
});

Hope this helps someone else.

I think I was looking more for direction in how asynchronous stuff is handled within the marble callback

Instead of commenting on a closed issue, it's better to open a new issue, clearly state your problem or question, and reference any issues that you think are related.

Pretty much all maintainers that I know share this opinion.

Apologies good point. Anyway I was just finishing my thought. Feel free to lock the issue.