第一次失败后开玩笑停止测试套件

Posted

技术标签:

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

我正在使用Jest 进行测试。

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

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

【问题讨论】:

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

【讨论】:

以上是关于第一次失败后开玩笑停止测试套件的主要内容,如果未能解决你的问题,请参考以下文章

Xcode UI 测试 - 当测试套件中的任何给定测试失败时停止测试?

开玩笑:测试套件无法运行,意外令牌 =

用 Create React App 开玩笑:测试套件无法运行 - 意外的令牌

用开玩笑的结果简单测试一个 vue 组件失败

开玩笑 你的测试套件必须至少包含一个测试

开玩笑:测试套件无法运行,SyntaxError:意外的令牌导入