jest icon indicating copy to clipboard operation
jest copied to clipboard

Jest should fail fast and exit early (change request for --bail)

Open alycda opened this issue 7 years ago • 28 comments
trafficstars

🚀 Feature Proposal

Jest should not run subsequent hooks or tests associated with that hook if an exception is thrown in a hook.

Motivation

When setup/teardown is broken up into small chunks (open browser, navigate to url, login, etc) and one of those hooks fails, all subsequent hooks and tests may fail and the output is clogged.

Example

When using Jest with puppeteer, I need to do the following before running any tests:

  1. open browser
  2. navigate to URL
  3. login
  4. wait for selector/page to be loaded

If any of those steps fail, the subsequent steps and tests are likely to fail as well.

Pitch

I'm using jest because of the detailed reporter, especially when a test fails. But failure reports can become needlessly long when it's only the first item that needs to be fixed to prevent a long subsequent list of failures/errors

I do not believe this is considered a change to the default reporter, as --bail does not behave the way I expect it to (it does stop after the first failing test, but if the test has 5 hooks, it still tries to run subsequent hooks and then the test fails).

alycda avatar Jun 23 '18 00:06 alycda

For those that want an immediate solution to this problem: https://github.com/facebook/jest/issues/2867

jefftangx avatar Mar 25 '19 17:03 jefftangx

The suggested solution works, as long as you're not using jest-circus, you have to still be using Jasmine under the hood. Is there any word on this functionality making it's way into jest-circus as an option?

seeruk avatar Jun 25 '19 12:06 seeruk

Hello, everyone, I'm looking forward to helping with this if needed 😊

My impression is that this is actually a bug rather than a feature request.

It's also important to separate the two issues I see here:

  1. The one described by @seeruk, which is about the workaround described in #2867 not working anymore. (This might not be a fault on Jest's side, but on how the workaround expected it to work)
  2. The fact that if a beforeAll/beforeEach hook fails with the --bail CLI option, the related tests still run.

I was able to reproduce (2) using [email protected].

describe("test bail", () => {
    // These will run regardless of the success of `beforeEach` or any other hooks
    beforeEach(() => { throw new Error("beforeEach hook fails!") });
    test("first", () => expect(true).toBe(true));
    test("second", () => expect(true).toBe(true));
    test("third", () => expect(true).toBe(true));
});

I think it's reasonable to expect that if beforeAll and beforeEach fail when using the --bail flag no subsequent tests in that same suite should run. IMO that's also what most people would expect to happen given the number of upvotes in this issue (but I'm happy to be proven wrong if that's the case).

For the sake of symmetry and coherence I think that the same should happen with afterAll and afterEach.

I'd be happy to send a PR for solving (2) if the maintainers agree that should be the desired behaviour 💖

lucasfcosta avatar Jun 26 '19 08:06 lucasfcosta

For the sake of symmetry and coherence I think that the same should happen with afterAll and afterEach.

It might only make sense for afterEach to fail the suite if --bail is used. afterAll is well... after all of the suite has run anyway.

I think my request is more of a feature request for circus. It'd be nice to be able to tell it to end a suite if a single test in it fails. My main use-case for this (which I'm using the workaround for with jest-jasmine currently) is E2E testing. There's not much point in continuing if an earlier test fails in a suite, because the rest will undoubtedly fail too. Then my custom reporter would spam Slack with all of the errors...

seeruk avatar Jun 26 '19 08:06 seeruk

If beforeEach fails, the test should not run regardless of the bail flag. That's supposed to be the behaviour of Circus, but that's apparently broken (I'm on mobile, hard to track down the link).

We'll be removing Jasmine at some point (it'll stop being the default in the next major) so we're not really adding features to it, but happy to take a PR though if you wanna do the work :)


And I agree bail should work on the whole test run. Possibly behind an option if you want it to only short circuit per test file (current behaviour). Or switch the behaviour and make the current one be behind a new option

SimenB avatar Jun 26 '19 08:06 SimenB

@all @SimenB I'd love to try tackling this if possible 😊

If I understood correctly, as per your explanation here's a list of changes I intend to make if you agree they should be in:

  1. If beforeEach fails, the test should not run regardless of the bail flag
  2. Add a new flag for the current behaviour of bail which is to short-circuit per test file. This is what I'd recommend since to me it seems like bail should be general and there should be a second bailPerFile flag which encapsulates the current behaviour.

A third one would be to make #2867 work in jest-circus, but I'd like to focus on the two above first since I don't want to bite more than I can chew 😄

lucasfcosta avatar Jun 27 '19 06:06 lucasfcosta

@lucasfcosta, given the above suggestions, would --bail then stop a suite when the first test fails? That's the behaviour I'm pretty keen on. Thanks for looking into tackling these changes.

seeruk avatar Jun 27 '19 09:06 seeruk

Just an update on this:

As I was investigating today, it seems like @SimenB was correct in regards to the tests not running if the beforeEach hook fails in jest-circus. However, in Jasmine, it still runs the tests even after beforeEach fails.

Running the following describe block without JEST_CIRCUS=1 will throw the errors inside each file:

describe('a block', () => {
  beforeEach(() => {
    throw new Error('The beforeEach hook failed.');
  });

  test('skipped first test', () => {
    throw new Error('First test should not have been run');
  });

  test('skipped second test', () => {
    throw new Error('Second test should not have been run');
  });
});

With jest-circus I get beforeEach hook fails! for each of the tests, but without it, I do get the respective error messages.

Given the explanation above, aborting tests in case the beforeEach hook fails is a fix which is only necessary for Jasmine.

I haven't yet got to the item number 2 in this comment but I'll keep updates contained in this issue (preferably editing this post).

PS: I'd rather report updates here since then I can link to the discussion in the PR and someone else can pick up the work if they need it, but if you think this is unnecessary noise in GH notifications just let me know and I'll avoid updates before the PR.

lucasfcosta avatar Jul 03 '19 19:07 lucasfcosta

And I agree bail should work on the whole test run. Possibly behind an option if you want it to only short circuit per test file (current behaviour). Or switch the behaviour and make the current one be behind a new option

Is this change to the bail flag going to be implemented in jest-circus and is there somewhere that work is being tracked?

krististepanek avatar Oct 24 '19 22:10 krististepanek

I had a test case failing only when running after other test cases. I had to comment all following test cases to debug this because jest does not have an option to actually bail after 1 failed test case and not test suite.

aalexgabi avatar Feb 14 '20 18:02 aalexgabi

Running npx jest --bail for the following file still prints out the console.log statements:

describe('a block', () => {
    beforeEach(() => {
        throw new Error('The beforeEach hook failed.');
    });

    test('skipped first test', () => {
        console.log('XXX skipped first test');
        // throw new Error('First test should not have been run');
        expect(true).toBe(true);
    });

    test('skipped second test', () => {
        console.log('XXX skipped second test');
        // throw new Error('Second test should not have been run');
        expect(true).toBe(true);
    });
});

Logs:

 FAIL  e2e/auth/test.e2e-spec.ts
  a block
    ✕ skipped first test (4ms)
    ✕ skipped second test (1ms)

  ● a block › skipped first test

    The beforeEach hook failed.

      1 | describe('a block', () => {
      2 |     beforeEach(() => {
    > 3 |         throw new Error('The beforeEach hook failed.');
        |               ^
      4 |     });
      5 | 
      6 |     test('skipped first test', () => {

      at Object.<anonymous> (e2e/auth/test.e2e-spec.ts:3:15)

  ● a block › skipped second test

    The beforeEach hook failed.

      1 | describe('a block', () => {
      2 |     beforeEach(() => {
    > 3 |         throw new Error('The beforeEach hook failed.');
        |               ^
      4 |     });
      5 | 
      6 |     test('skipped first test', () => {

      at Object.<anonymous> (e2e/auth/test.e2e-spec.ts:3:15)

  console.log e2e/auth/test.e2e-spec.ts:7
    XXX skipped first test

  console.log e2e/auth/test.e2e-spec.ts:13
    XXX skipped second test

This indicates to me that the tests are still run even though it says "skipped"

BorntraegerMarc avatar Apr 15 '20 15:04 BorntraegerMarc

It would be great to have a way to mark tests as dependent on previous step for e2e test as described here.

I think the simplest implementation might be a test.step function that will skip tests if a previous step in the describe has failed. I'd be happy to look into implementing this for jest-circus, but there needs to be agreement about the design first. This issue is not very specific about whether it treats beforeAll issues or just wants a way to mark tests as dependent.

MrLoh avatar May 01 '20 17:05 MrLoh

Consider using "bail" config option:

https://jestjs.io/docs/en/configuration#bail-number--boolean

This works for cases when you'd want a fast fail in beforeAll / beforeEach setup blocks.

imankovskyi avatar Jul 21 '20 08:07 imankovskyi

Not the same as already explained several times in these Threads

MrLoh avatar Jul 21 '20 12:07 MrLoh

@seeruk I've got a jest-circus solution I think works:

  1. Define CustomEnvironment.js
const JestEnvironmentNode = require("jest-environment-node");

class CustomEnvironment extends JestEnvironmentNode {
  /**
   * @param {import('jest-circus').Event} event
   * @param {import('jest-circus').State} state
   */
  async handleTestEvent(event, state) {
    if (event.name === 'test_fn_failure' || event.name === 'hook_failure') {
      // re-throw the error to exit immediately
      throw new Error(event.error);
    }
  }
}

module.exports = CustomEnvironment;
  1. Use Custom Environment in jest.config.js
module.exports = {
  testRunner: 'jest-circus/runner',
  testEnvironment: './CustomEnvironment.js',
  …
};

wilsonpage avatar Nov 27 '20 16:11 wilsonpage

Skip all after first failure

Before: image After: image

I run test with --runInBand

  1. Define a custom Environment: jest-environment-fail-fast.js
const ParentEnvironment = require('jest-environment-node') 
// or require('jest-environment-jsdom')
// or require('jest-playwright-preset/lib/PlaywrightEnvironment').default

class JestEnvironmentFailFast extends ParentEnvironment {
    failedTest = false;
    
    async handleTestEvent(event, state) {
        if (event.name === 'hook_failure' || event.name === 'test_fn_failure') {
            this.failedTest = true;
        } else if (this.failedTest && event.name === 'test_start') {
            event.test.mode = 'skip';
        }

        if (super.handleTestEvent) {
            await super.handleTestEvent(event, state)
        }
    }
}

module.exports = JestEnvironmentFailFast
  1. In jest.config.js, testRunner must be 'jest-circus/runner' and: testEnvironment: './jest-environment-fail-fast.js',

More events names in https://github.com/facebook/jest/blob/master/packages/jest-circus/src/run.ts, dispatch functions.

davidglezz avatar Jan 14 '21 10:01 davidglezz

This jasmine-fail-fast hack still works: https://github.com/facebook/jest/issues/2867#issuecomment-370624846

arlowhite avatar Feb 17 '21 01:02 arlowhite

@davidglezz Surely you mean

await super.handleTestEvent(event, state)

in your environment code, right?

(Edit: Thanks for applying the fix!)

brady-ds avatar Jul 24 '21 21:07 brady-ds

Totally agree with https://github.com/facebook/jest/issues/6527#issuecomment-505772215.

Tests should stop running if before... has an exception/error. Your test setup conditions failed so how can you possibly run the expectations?

rcollette avatar Jun 22 '22 16:06 rcollette

For the solution posted by @davidglezz, it seems that [(for jest-environment-node, at minimum)] it is necessary[^a] to add .TestEnvironment for it to work properly.[^b]

(edited as indicated to add clarification of context for the guidance)

[^a]:[I was using the solution in the context of jest-environment-node, and] without that addition I encountered a runtime error, while with the addition it worked as [I] expected/desired.

[^b]: This is supported by https://github.com/facebook/jest/blob/26bd03a77d6b683632adaf0a45ec912c0cd707a0/e2e/test-environment-circus/CircusHandleTestEventEnvironment.js#L10

vjjft avatar Mar 10 '23 01:03 vjjft

@vjjft in @davidglezz solution, it imports jest-environment-node while in your provided link it imports jest-environment-jsdom. I didn't check the implementation of both packages, but I guess that's the reason for the difference you encountered.

zirkelc avatar Mar 10 '23 07:03 zirkelc

@zirkelc I was/am only working with jest-environment-node, so I believe it is needed in both cases. I have edited my previous comment to provide that context.

vjjft avatar Mar 10 '23 13:03 vjjft

In case someone using Jest with Typescript would like a TS native solution adapted from the https://github.com/facebook/jest/issues/6527#issuecomment-760092817 by @davidglezz, this is working for me:

import type { Circus, } from "@jest/types"
import { TestEnvironment, } from "jest-environment-node"
// import TestEnvironment from "jest-environment-jsdom"

class FailFastEnvironment extends TestEnvironment
{
  failedTest = false

  async handleTestEvent(
    event: Circus.Event,
    state: Circus.State,
  )
  {
    if ( event.name === "hook_failure" || event.name === "test_fn_failure" )
    {
      this.failedTest = true
    } else if ( this.failedTest && event.name === "test_start" )
    {
      event.test.mode = "skip"
    }

    // @ts-ignore
    if ( super.handleTestEvent ) await super.handleTestEvent( event, state, )
  }
}

export default FailFastEnvironment

vjjft avatar Mar 10 '23 15:03 vjjft

@vjjft it works perfectly ! clean and type-safe. thank you.

jeanhdev avatar Apr 05 '23 11:04 jeanhdev

This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days.

github-actions[bot] avatar Apr 04 '24 12:04 github-actions[bot]

Unstale

gladykov avatar Apr 04 '24 12:04 gladykov

Should there just be a --full-bail option along with the --bail option? I think my confusion about how --bail works revolves around what test suite means, maybe this is similar to other peoples experience. In my head, it meant all of the tests associated to some project. But, it seems that it actually means all of the tests in a single file. So, using --bail may fail a specific file's test collection quickly, but, it doesn't do anything when it comes to the tests in other files in the same project.

jdfm avatar Apr 18 '24 16:04 jdfm

Any updates on this?

rogerprz avatar Jun 28 '24 17:06 rogerprz