Skip to content

Testing RxJS Side Effects

Posted on:January 30, 2019 at 08:22 PM

Often when writing tests for functions returning RxJS Observables (for example while writing reactive-graphql) I found myself in a situation where I really enjoyed marble testing. It gives you a great way to express expectations around the timings of emitted data and the data itself, so naturally, I write most of my test cases with this framework.

One thing I always struggled with was how to express side-effects of subscribing to an observable in tests. For example I would like to know if by subscribing to an observable a certain method was called with the right arguments. Let’s see an example for this.

it("has a side-effect", marbles(m => {
  const filterFunction = jest.fn();
  const outputObservable = myFunction(filterFunction);
  const expected = m.cold("----x--|", {
     x: "hello world"
  });
  m.expect(outputObservable).toBeObservable(expected);

  // Does not work, because these are executed before
  // m.expect subscribes to the observable
  expect(filterFunction).toHaveBeenCalledWith("hello");
  expect(filterFunction).toHaveBeenCalledWith("world");
}));

Now this is a great pattern for pure functions, but following this pattern it’s hard to write a test which checks on the filter function. To overcome this we need to make use of jest waiting for any promise that is returned inside of the test function. Testing if filterFunction was called with the right arguments we can write a test like this.

it("has a side-effect", marbles(m => {
  const filterFunction = jest.fn();
  const outputObservable = myFunction(filterFunction);
  const expected = m.cold("----x--|", {
     x: "hello world"
  });
  m.expect(outputObservable).toBeObservable(expected);

  return expected.toPromise().then(() => {
    expect(filterFunction).toHaveBeenCalledWith("hello");
    expect(filterFunction).toHaveBeenCalledWith("world");
  });
}));

With this approach, you don’t get the proper error messages as the assertions are outside of the actual tests and therefore just shown as uncaught errors. To get proper error messages you need to go one step further and use async/await:

it("has a side-effect", marbles(async m => {
  const filterFunction = jest.fn();
  const outputObservable = myFunction(filterFunction);
  const expected = m.cold("----x--|", {
     x: "hello world"
  });
  m.expect(outputObservable).toBeObservable(expected);

  await expected.toPromise();
  expect(filterFunction).toHaveBeenCalledWith("hello");
  expect(filterFunction).toHaveBeenCalledWith("world");
}));