【问题标题】:Jest stop test suite after first fail第一次失败后开玩笑停止测试套件
【发布时间】:2018-12-17 09:37:18
【问题描述】:

我正在使用Jest 进行测试。

我想要的是在该测试套件中的测试失败时停止执行当前测试套件。

--bail option 不是我需要的,因为它会在一个测试套件失败后停止其他测试套件。

【问题讨论】:

  • 这是一个很好的问题,我很惊讶 Jest 似乎没有针对这种情况的指导。我已经提交了ticket on their GitHub repo
  • @DanDascalescu 我认为这更多是关于抛出错误的钩子。正如 OP 询问的那样,如果测试失败,我们该怎么办?

标签: javascript unit-testing testing jestjs


【解决方案1】:

我做了一些杂碎,但它对我有用。

stopOnFirstFailed.js:

/**
 * This is a realisation of "stop on first failed" with Jest
 * @type {{globalFailure: boolean}}
 */

module.exports = {
    globalFailure: false
};

// Injects to jasmine.Spec for checking "status === failed"
!function (OriginalSpec) {
    function PatchedSpec(attrs) {
        OriginalSpec.apply(this, arguments);

        if (attrs && attrs.id) {
            let status = undefined;
            Object.defineProperty(this.result, 'status', {
                get: function () {
                    return status;
                },
                set: function (newValue) {
                    if (newValue === 'failed') module.exports.globalFailure = true;
                    status = newValue;
                },
            })
        }
    }

    PatchedSpec.prototype = Object.create(OriginalSpec.prototype, {
        constructor: {
            value: PatchedSpec,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });

    jasmine.Spec = PatchedSpec;
}(jasmine.Spec);

// Injects to "test" function for disabling that tasks
test = ((testOrig) => function () {
    let fn = arguments[1];

    arguments[1] = () => {
        return module.exports.globalFailure ? new Promise((res, rej) => rej('globalFailure is TRUE')) : fn();
    };

    testOrig.apply(this, arguments);
})(test);

在所有测试之前导入该文件(在第一个 test(...) 之前),例如我的 index.test.js

require('./core/stopOnFirstFailed'); // before all tests

test(..., ()=>...);
...

当第一个错误发生时,该代码将所有接下来的测试failed 标记为globalFailure is TRUE

如果您想排除failing,例如。您可以这样做一些清理测试:

const stopOnFirstFailed = require('../core/stopOnFirstFailed');

describe('some protected group', () => {
    beforeAll(() => {
        stopOnFirstFailed.globalFailure = false
    });
    test(..., ()=>...);
    ...

它从failing 中排除整个组。

使用 Node 8.9.1 和 Jest 23.6.0 测试

【讨论】:

  • 不适用于node v10.15.3jest v24.9.0。我得到TypeError: Class constructor Spec cannot be invoked without 'new' 指向function PatchedSpec(attrs) { 行。为什么你们的 cmets 会提到 Jasmine?
  • 我什至没有尝试过,因为它看起来很糟糕......从整个测试套件中基本上只在 Jest 中进行 process.exit() 有那么复杂吗?!
【解决方案2】:

我有连续且复杂的测试场景,如果该套件的其中一个测试失败,则没有必要继续测试套件。但是我没有设法将它们标记为已跳过,因此它们显示为已通过。

我的测试套件示例:

describe('Test scenario 1', () => {

test('that item can be created', async () => {
    expect(true).toBe(false)
})

test('that item can be deleted', async () => {
    ...
})
...

我改成如下:

let hasTestFailed = false
const sequentialTest = (name, action) => {
    test(name, async () => {        
      if(hasTestFailed){
        console.warn(`[skipped]: ${name}`)} 
      else {
          try {         
            await action()} 
          catch (error) {           
            hasTestFailed = true
            throw error}            
      }
    })
  }
describe('Test scenario 1', () => {
    
sequentialTest('that item can be created', async () => {
        expect(true).toBe(false)
})
    
sequentialTest('that item can be deleted', async () => {
        ...
})

如果第一个测试失败,下一个测试将不会运行,但它们将获得通过状态。

报告将如下所示:

  • 测试场景 1 > 可以创建该项目 - 失败
  • 测试场景 1 > 可以删除该项目 - 通过

这并不理想,但在我的情况下可以接受,因为我只想在报告中看到失败的测试。

【讨论】:

    【解决方案3】:

    感谢this comment on github,我能够使用自定义testEnvironment 解决此问题。为此,jest-circus 需要通过 npm/yarn 安装。
    值得注意的是jest will set jest-circus to the default runner with jest v27

    首先需要调整 jest 配置:

    jest.config.js

    module.exports = {
      rootDir: ".",
      testRunner: "jest-circus/runner",
      testEnvironment: "<rootDir>/NodeEnvironmentFailFast.js",
    }
    
    

    然后你需要实现一个自定义环境,上面的配置已经引用了这个环境:

    NodeEnvironmentFailFast.js

    const NodeEnvironment = require("jest-environment-node")
    
    class NodeEnvironmentFailFast extends NodeEnvironment {
      failedDescribeMap = {}
      registeredEventHandler = []
    
      async setup() {
        await super.setup()
        this.global.testEnvironment = this
      }
    
      registerTestEventHandler(registeredEventHandler) {
        this.registeredEventHandler.push(registeredEventHandler)
      }
    
      async executeTestEventHandlers(event, state) {
        for (let handler of this.registeredEventHandler) {
          await handler(event, state)
        }
      }
    
      async handleTestEvent(event, state) {
        await this.executeTestEventHandlers(event, state)
    
        switch (event.name) {
          case "hook_failure": {
            const describeBlockName = event.hook.parent.name
    
            this.failedDescribeMap[describeBlockName] = true
            // hook errors are not displayed if tests are skipped, so display them manually
            console.error(`ERROR: ${describeBlockName} > ${event.hook.type}\n\n`, event.error, "\n")
            break
          }
          case "test_fn_failure": {
            this.failedDescribeMap[event.test.parent.name] = true
            break
          }
          case "test_start": {
            if (this.failedDescribeMap[event.test.parent.name]) {
              event.test.mode = "skip"
            }
            break
          }
        }
    
        if (super.handleTestEvent) {
          super.handleTestEvent(event, state)
        }
      }
    }
    
    module.exports = NodeEnvironmentFailFast
    

    注意

    我添加了 registerTestEventHandler 功能,这对于快速失败功能来说不是必需的,但我认为它非常有用,尤其是如果您之前使用过 jasmine.getEnv() 并且它可以与 async/await 一起使用!
    您可以像这样在您的测试中注册自定义处理程序inside(例如beforeAll hook):

    // testEnvironment is globally available (see above NodeEnvironmentFailFast.setup)
    testEnvironment.registerTestEventHandler(async (event) => {
      if (event.name === "test_fn_failure") {
        await takeScreenshot()
      }
    })
    

    当一个test 失败时,将跳过同一describe 中的其他test 语句。这也适用于嵌套的 describe 块,但 describe必须具有不同的名称。

    执行以下测试:

    describe("TestJest 3 ", () => {
      describe("TestJest 2 ", () => {
        describe("TestJest 1", () => {
          beforeAll(() => expect(1).toBe(2))
          test("1", () => {})
          test("1.1", () => {})
          test("1.2", () => {})
        })
    
        test("2", () => expect(1).toBe(2))
        test("2.1", () => {})
        test("2.2", () => {})
      })
    
      test("3", () => {})
      test("3.1", () => expect(1).toBe(2))
      test("3.2", () => {})
    })
    

    将产生以下日志:

     FAIL  suites/test-jest.spec.js
      TestJest 3 
        ✓ 3
        ✕ 3.1 (1 ms)
        ○ skipped 3.2
        TestJest 2 
          ✕ 2
          ○ skipped 2.1
          ○ skipped 2.2
          TestJest 1
            ○ skipped 1
            ○ skipped 1.1
            ○ skipped 1.2
    
      ● TestJest 3  › TestJest 2  › TestJest 1 › 1
    
        expect(received).toBe(expected) // Object.is equality
    
        Expected: 2
        Received: 1
    
          2 |   describe("TestJest 2 ", () => {
          3 |     describe("TestJest 1", () => {
        > 4 |       beforeAll(() => expect(1).toBe(2))
            |                                 ^
          5 |       test("1", () => {})
          6 |       test("1.1", () => {})
          7 |       test("1.2", () => {})
    
          at suites/test-jest.spec.js:4:33
    
      ● TestJest 3  › TestJest 2  › TestJest 1 › 1.1
    
        expect(received).toBe(expected) // Object.is equality
    
        Expected: 2
        Received: 1
    
          2 |   describe("TestJest 2 ", () => {
          3 |     describe("TestJest 1", () => {
        > 4 |       beforeAll(() => expect(1).toBe(2))
            |                                 ^
          5 |       test("1", () => {})
          6 |       test("1.1", () => {})
          7 |       test("1.2", () => {})
    
          at suites/test-jest.spec.js:4:33
    
      ● TestJest 3  › TestJest 2  › TestJest 1 › 1.2
    
        expect(received).toBe(expected) // Object.is equality
    
        Expected: 2
        Received: 1
    
          2 |   describe("TestJest 2 ", () => {
          3 |     describe("TestJest 1", () => {
        > 4 |       beforeAll(() => expect(1).toBe(2))
            |                                 ^
          5 |       test("1", () => {})
          6 |       test("1.1", () => {})
          7 |       test("1.2", () => {})
    
          at suites/test-jest.spec.js:4:33
    
      ● TestJest 3  › TestJest 2  › 2
    
        expect(received).toBe(expected) // Object.is equality
    
        Expected: 2
        Received: 1
    
           8 |     })
           9 | 
        > 10 |     test("2", () => expect(1).toBe(2))
             |                               ^
          11 |     test("2.1", () => {})
          12 |     test("2.2", () => {})
          13 |   })
    
          at Object.<anonymous> (suites/test-jest.spec.js:10:31)
    
      ● TestJest 3  › 3.1
    
        expect(received).toBe(expected) // Object.is equality
    
        Expected: 2
        Received: 1
    
          14 | 
          15 |   test("3", () => {})
        > 16 |   test("3.1", () => expect(1).toBe(2))
             |                               ^
          17 |   test("3.2", () => {})
          18 | })
          19 | 
    
          at Object.<anonymous> (suites/test-jest.spec.js:16:31)
    
    Test Suites: 1 failed, 1 total
    Tests:       2 failed, 6 skipped, 1 passed, 9 total
    Snapshots:   0 total
    Time:        0.638 s, estimated 1 s
    
    

    【讨论】:

      【解决方案4】:

      这是我的solution -- 如果有重大缺点,请告诉我,就我的目的而言,它似乎按预期工作

      我只有一个顶级描述块,出于我的目的,我希望在一个测试失败时整个测试文件都失败

      export class FailEarly {
        msg: string | undefined;
        failed: boolean = false;
        jestIt: jest.It;
      
        constructor(jestIt: jest.It) {
          this.jestIt = jestIt;
        }
      
        test = (name: string, fn: jest.EmptyFunction, timeout?: number) => {
          const failEarlyFn = async () => {
            if (this.failed) {
              throw new Error(`failEarly: ${this.msg}`);
            }
      
            try {
              await fn();
            } catch (error) {
              this.msg = name;
              this.failed = true;
              throw error;
            }
          };
      
          this.jestIt(name, failEarlyFn, timeout);
        };
      }
      

      给我一​​个上下文(类属性)来存储 global-esq 变量

      const failEarlyTestRunner = new FailEarly(global.it);
      
      const test = failEarlyTestRunner.test;
      const it = failEarlyTestRunner.test;
      

      用我的类方法重载testit 函数(从而访问类属性)

      describe('my stuff', () => {
        it('passes', async () => {
          expect(1).toStrictEqual(1);
        })
      
        test('it fails', async () => {
          expect(1).toStrictEqual(2);
        })
      
        it('is skipped', async () => {
          expect(1).toStrictEqual(1);
        })
      })
      
      

      结果:

      my stuff
        ✓ can create a sector (2 ms)
        ✕ it fails (2 ms)
        ✕ is skipped (1 ms)
      
      
        ● my stuff › it fails
      
          expect(received).toStrictEqual(expected) // deep equality
      
          Expected: 2
          Received: 1
      
          > ### |       expect(1).toStrictEqual(2);
                |                 ^
            ### |     });
      
      
        ● my stuff › is skipped
      
          failEarly: it fails
      
            69 |     const failEarlyFn = async () => {
            70 |       if (this.failed) {
          > 71 |         throw new Error(`failEarly: ${this.msg}`);
               |               ^
            72 |       }
            73 |
            74 |       try {
      
      

      每个跳过的测试都失败,并带有一个错误,指示上游失败的测试

      正如其他人指出的那样——你必须用--runInBand 标志开玩笑

      希望这对某人有所帮助-如果有有意义的缺点或更好的方法,请发表评论;我总是乐于学习

      【讨论】:

        【解决方案5】:

        破解 global.jasmine.currentEnv_.fail 对我有用。

              describe('Name of the group', () => {
        
                beforeAll(() => {
        
                  global.__CASE_FAILED__= false
        
                  global.jasmine.currentEnv_.fail = new Proxy(global.jasmine.currentEnv_.fail,{
                    apply(target, that, args) {
                      global.__CASE__FAILED__ = true
                      // you also can record the failed info...
                      target.apply(that, args)
                      }
                    }
                  )
        
                })
        
                afterAll(async () => {
                  if(global.__CASE_FAILED__) {
                    console.log("there are some case failed");
                    // TODO ...
                  }
                })
        
                it("should xxxx", async () => {
                  // TODO ...
                  expect(false).toBe(true)
                })
              });
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-02-13
          • 2021-03-19
          • 2017-11-19
          • 2020-02-24
          • 1970-01-01
          • 1970-01-01
          • 2020-10-08
          相关资源
          最近更新 更多