12

I've been trying to get Jest working with RxJS and am having trouble with Jest not propagating errors from inside the subscribe callback.

Here is a sample test I've tried that is not working:

import {of} from 'rxjs';

test('should fail', () => {
  of(false).subscribe(val => {
    expect(val).toBe(true);
  });
});

The above test should fail, but it passes. I've googled around and found the following solution:

This suggests using the "done" syntax in jest to solve the issue. While using the "done" callback does get the above test to fail, there are a number of issues with this approach:

Undescriptive errors

The test fails because the 'expect' call in subcribe() throws an error, resulting in 'done()' never getting called. The test then times out, waiting for done. So instead of propagating the 'expect' error, it is causing a timeout error, which means every test that fails in the expect clause will show a timeout error instead of the actual error message of the failed 'expect' call.

Tests take longer to fail

Because all tests are failing due to a timeout error, that means it takes 5 seconds for each test to fail (async tests timeout after 5 seconds). This can dramatically increase the amount of time for tests to run

Poor use of done

The done callback is meant to support asynchronous use cases for testing. But rxjs is not necessarily asynchronous. The code I inlined above actually runs synchronously. For example, the following test will pass:

import {of} from 'rxjs';

test('should pass', () => {
  let didRunSynchronously = false;
  of(true).subscribe(() => {
    didRunSynchronously = true;
  });
  expect(didRunSynchronously).toBe(true);
});

It seems strange to have to use asynchronous semantics to solve a problem for a synchronous test.

Wondering if anyone has come up with a good solution for testing in rxjs that will result in the expect calls to properly get handled by the testing library.

Thanks in advance!

Relevant dependencies in package.json:

 "dependencies": {
    "@babel/polyfill": "^7.0.0",
    "classnames": "^2.2.6",
    "history": "^4.7.2",
    "json-stringify-pretty-compact": "^1.2.0",
    "minimist": "^1.2.0",
    "normalize.css": "^8.0.0",
    "nullthrows": "^1.1.0",
    "react": "^16.5.2",
    "react-dom": "^16.5.2",
    "react-router-dom": "^4.3.1",
    "rxjs": "^6.3.3",
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/plugin-proposal-class-properties": "^7.1.0",
    "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
    "@babel/preset-env": "^7.1.0",
    "@babel/preset-flow": "^7.0.0",
    "@babel/preset-react": "^7.0.0",
    "babel-core": "^7.0.0-bridge.0",
    "babel-env": "^2.4.1",
    "babel-eslint": "^10.0.1",
    "babel-jest": "^23.6.0",
    "babel-loader": "^8.0.4",
    "copy-webpack-plugin": "^4.5.3",
    "css-loader": "^1.0.0",
    "eslint": "^5.9.0",
    "eslint-plugin-flowtype": "^3.2.0",
    "eslint-plugin-import": "^2.14.0",
    "eslint-plugin-react": "^7.11.1",
    "eslint-watch": "^4.0.2",
    "flow-bin": "^0.83.0",
    "html-webpack-plugin": "^3.2.0",
    "jest": "^23.6.0",
    "prettier": "^1.15.3",
    "style-loader": "^0.23.1",
    "webpack": "^4.20.2",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.9"
  }

.babelrc file

{
  "plugins": [
    "module:@babel/plugin-proposal-class-properties",
    "module:@babel/plugin-proposal-object-rest-spread"
  ],
  "presets": [
    ["module:@babel/preset-env", { "targets": { "node": "6.10" } }],
    "module:@babel/preset-flow"
  ]
}
Liam
  • 27,717
  • 28
  • 128
  • 190
brenmcnamara
  • 459
  • 3
  • 10

3 Answers3

7

Even if i am some years later on this topic, this might help others that are new to testing async code. Please refer for example to https://jestjs.io/docs/asynchronous and use a done() callback at the end of your subscription. If this callback is not executed, because of the error before, the test will fail as expected.

it('should fetch the right data', done => {
  fetchData().subscribe(data => {
    expect(data).toBe('expected data');
    done();
  });
});
user3840527
  • 71
  • 1
  • 2
5

Figured out the problem! Leaving this here for anyone who runs into a similar issue. RxJS and Jest were working properly and propagating the errors correctly. The problem was that I added a "jest.useFakeTimers" call to the testing script. For some reason, this was causing the errors not to propagating properly in the test. I needed to add "jest.runAllTimers" to get the errors to throw. Here is the full test script, implemented correctly:

import {of} from 'rxjs';

jest.useFakeTimers();

test('should fail', () => {
  of(false).subscribe(val => {
    expect(val).toBe(true);
  });
  jest.runAllTimers();
});

If you don't need mock timers, then no need to add them. I thought it was a bit strange that fake timers were an issue even though I could verify the code was getting called synchronously. If someone has more insight on the implementation details of why this is the case, I'd appreciate some explanation.

Liam
  • 27,717
  • 28
  • 128
  • 190
brenmcnamara
  • 459
  • 3
  • 10
  • 2
    `subscribe` will never throw synchronously. See: https://github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/v6/migration.md#synchronous-error-handling – cartant Dec 18 '18 at 11:54
1

I'll complete answer from @user3840527 as you're already mentionning in your question the use of done and the issues it caused (Undescriptive errors & Tests take longer to fail), and this is the part where I also had problems.

The solution is that you also need, as explained in the documentation at https://jestjs.io/docs/asynchronous#callbacks but maybe not highlighted enough and maybe it was not the case before, to wrap the expect calls in a try/catch block, so the error are properly displayed and without the timeout.

So your example becomes:

test('should fail', (done) => {
  of(false).subscribe(val => {
    try {
      expect(val).toBe(true);
      done();
    } catch (error) {
      done(error);
    }
  });
});
Kévin Barré
  • 330
  • 1
  • 4
  • 11