jest: add option to fail tests that print errors and warnings

Do you want to request a feature or report a bug?

Feature.

What is the current behavior?

Tests pass even if they print errors and warnings to the console.

What is the expected behavior?

It would be nice if we could force the test to fail.

Our test suite prints lots of things like this:

    console.warn node_modules/moment/moment.js:293
      Deprecation warning: value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.
    console.error node_modules/fbjs/lib/warning.js:33
      Warning: Each child in an array or iterator should have a unique "key" prop.
(node:15601) UnhandledPromiseRejectionWarning: SyntaxError: The string did not match the expected pattern.
    console.error node_modules/fbjs/lib/warning.js:33
      Warning: `value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.

These are all legitimate errors that we should fix, yet our automated tests pass. If Jest could be made to fail, that would encourage developers to keep the tests clean.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 7
  • Comments: 24 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Here’s the variant I landed on, using Node’s Util.format function to preserve console string formatting via arguments in the error output:

// setupTests.js
import { format } from "util";

const error = global.console.error;

global.console.error = function(...args) {
  error(...args);
  throw new Error(format(...args));
};

cc @akx, re:

Error: Warning: Received %s for a non-boolean attribute %s.

This can already be achieved with a couple lines of code.

const spy = jest.spyOn(global.console, 'error')
// ...
expect(spy).not.toHaveBeenCalled();

@apaatsio how would you suggest someone implement this so that it runs across all test suites without having to include it in each individual test suite?

To get it working I’ve ended up with this ugly thing

beforeEach(() => {
    isConsoleWarningOrError = false;
    const originalError = global.console.error;
    jest.spyOn(global.console, 'error').mockImplementation((...args) => {
        isConsoleWarningOrError = true;
        originalError(...args);
    });
    const originalWarn = global.console.warn;
    jest.spyOn(global.console, 'warn').mockImplementation((...args) => {
        isConsoleWarningOrError = true;
        originalWarn(...args);
    });
});

afterEach(() => {

    if (isConsoleWarningOrError) {
        throw new Error('Console warnings and errors are not allowed');
    }
});

requirements:

  1. still log the warning or error like it does currently
  2. don’t add a new stack trace to it
  3. run automatically - don’t have to add to every spec
  4. still allow tests to mock console and add their own implementations and assert number of calls (this is why I can’t just expect it to be called - our own logging code mocks console and tests the fn calls)

Is there a preferred way to do this globally that doesn’t involve adding a beforeEach/All or afterEach/All to every test file? I want all tests to fail if any console.error is called.

I’m looking at the solution from here, which seems to do the job:

let error = console.error

console.error = function (message) {
  error.apply(console, arguments) // keep default behaviour
  throw (message instanceof Error ? message : new Error(message))
}

@lukeapage Thank you for contributing that solution (https://github.com/facebook/jest/issues/6121#issuecomment-412063935) – just a note about it:

You’re going to get Maximum Call Stack Size Exceeded when you call originalError/originalWarning, because you’re still spying on the parent global.console object, so it recurses.

May I suggest either using console.log in their place if you want the whole stack trace, or just letting jest spit out the original error/warning to stdout, and throwing the error as usual?

See below for revised code:

let isConsoleWarningOrError;

beforeEach(() => {
  isConsoleWarningOrError = false;
  jest.spyOn(global.console, 'error').mockImplementation((...args) => {
    isConsoleWarningOrError = true;
    // Optional: I've found that jest spits out the errors anyways
    console.log(...args);
  });
  jest.spyOn(global.console, 'warn').mockImplementation((...args) => {
    isConsoleWarningOrError = true;
    // Optional: I've found that jest spits out the errors anyways
    console.log(...args);
  });
});

afterEach(() => {
  if (isConsoleWarningOrError) {
    throw new Error('Console warnings and errors are not allowed');
  }
});

Is there a preferred way to do this globally that doesn’t involve adding a beforeEach/All or afterEach/All to every test file? I want all tests to fail if any console.error is called.

I’m looking at the solution from here, which seems to do the job:

let error = console.error

console.error = function (message) {
  error.apply(console, arguments) // keep default behaviour
  throw (message instanceof Error ? message : new Error(message))
}

When I tried this option I got an error on executing because of Create React App:

image

So I wrote a bash script to for manually looking for consoles and warnings (it can be found here).

Later I discovered that options not supported by React Create App on jest configurations can be used via the command line. So one could run:

npm test -- --setupFiles './tests/jest.overrides.js'

instead of changing package.json, and the solution pointed by @stevula would work.

Thanks, @stevula – that’s close, but the problem is that the %s interpolation supported by the console isn’t supported by thrown messages:

Error: Warning: Received `%s` for a non-boolean attribute `%s`.

If you want to write it to the DOM, pass a string instead: %s="%s" or %s={value.toString()}.%s

You can see the interpolated message too, but it’s a little cumbersome. 😃

setupFiles, you can throw instead of assert

Building up on @axelboc’s variant:

// check if console was used
let usedConsole = false;
['log', 'error', 'warn'].forEach((key) => {
  const originalFn = console[key];
  console[key] = (...args) => {
    usedConsole = true;
    originalFn(...args);
  };
});

// check if a test failed
// see https://stackoverflow.com/a/62557472/1337972
jasmine.getEnv().addReporter({
  specStarted: (result) => (jasmine.currentTest = result),
});

afterEach(() => {
  // if test hasn't failed yet, but console was used, we let it fail
  if (usedConsole && !jasmine.currentTest.failedExpectations.length) {
    usedConsole = false;
    throw `To keep the unit test output readable you should remove all usages of \`console\`.
They mostly refer to fixable errors, warnings and deprecations or forgotten debugging statements.

If your test _relies_ on \`console\` you should mock it:

\`\`\`
const errorSpy = jest.spyOn(console, 'error');
errorSpy.mockImplementation(() => {});
errorSpy.mockRestore();
\`\`\`
`;
  }
});

@stevula For what it’s worth, that’s exactly what works for me