如何在引擎盖下测试包含setTimeout / setInterval的代码

我们的开发人员非常喜欢单元测试,其用途显而易见。为了使这些测试真正有用,并且不会带来痛苦,有必要确保其稳定性。


我们公司开发了Wasaby接口框架,并销售在此基础上构建的产品,即云和桌面应用程序。释放周期紧紧地固定在日历上,并设置了连续的积分过程以控制产品的质量。我们将Jenkins用于装配体,并将MochaChai assert一起使用用于单元测试JavaScript代码。最近,我们面临着这样一种情况,即程序集监视开始显示,所有跌倒案例中约有一半是由于不稳定的JavaScript单元测试引起的。症状是相同的:从集中进行单独的测试要么没有时间运行,要么按预期返回了错误的结果。对案例的分析几乎总是揭示出这样一个事实,即包含对setTimeoutsetInterval函数的调用的测试本身或在测试的代码中崩溃关于在这种情况下该怎么办,我们将进一步讨论。


为什么会发生?


一切都很简单-在开发人员正在测试的代码内部,发生了一个异步调用,在此之后,环境状态以某种方式发生了变化。测试旨在验证此条件是否符合预期。但是由于 由于我们没有生活在理想的世界中,因此碰巧会为已经在战斗中工作的代码编写测试,而该代码最初并未为此类测试做好准备。开发人员必须决定如何测试这样的代码。


考虑一个简单的示例,测试代码是一个可异步更改实例状态的类方法。而且它写得并不完美:


export class Foo {
  propToTest: boolean = false;
  methodToTest(): void {
    setTimeout(() => {
      this.propToTest = true;
    }, 100);
  }
}

? , setTimeout . c , (, 101 100 ). :


it('should set propToTest to true', (done: Function) => {
  const inst = new Foo();
  inst.methodToTest();
  setTimeout(() => {
    assert.isTrue(inst.propToTest);
    done();
  }, 101);
});

? , , .. , callback, setTimeout, . — , 2 , . , . -, . , , - , — -.
setInterval.



, . , . , , .


  1. callback- , setTimeout/setInterval (" 1001 101, "). .
  2. setTimeout() (" event loop, "). .
  3. ( 1 2) — , , .

?


, , . , , : . .


  1. — .. .
    , , :


    export function setProperState(inst: Foo) {
      inst.propToTest = true;
    }
    
    export class Foo {
      propToTest: boolean = false;
      methodToTest(): void {
        setTimeout(() => {
          setProperState(this);
        }, 100);
      }
    }

    :


    it('should set propToTest to true', () => {
      const inst = new Foo();
      assert.isFalse(inst.propToTest);
      setProperState(inst);
      assert.isTrue(inst.propToTest);
    });

    , setProperState methodToTest ( ). , : , . , , .


  2. , , .
    , setTimeout :


    export class Foo {
      propToTest: boolean = false;
      constructor(readonly awaiter: Function = setTimeout) {
      }
      methodToTest(): void {
        this.awaiter(() => {
          this.propToTest = true;
        }, 100);
      }
    }

    -, :


    it('should set propToTest to true', () => {
      const awaiter = (callback) => callback();
      const inst = new Foo(awaiter);
      inst.methodToTest();
      assert.isTrue(inst.propToTest);
    });

  3. , Promise.
    , — , . . . :


    export class Foo {
      propToTest: boolean = false;
      methodToTest(): Promise<void> {
        return new Promise((resolve) => {
          setTimeout(() => {
            this.propToTest = true;
            resolve();
          }, 100);
         });
      }
    }

    setTimeout:


    it('should set propToTest to true', () => {
      const inst = newFoo();
      return inst.methodToTest().then(() => {
        assert.isTrue(inst.propToTest);
      });
    });

    , , , " ". setTimeout , .


    export class Foo {
      propToTest: boolean = false;
      constructor(readonly asyncTimeout: number = 100) {
      }
      methodToTest(): void {
        return new Promise((resolve) => {
          setTimeout(() => {
            this.propToTest = true;
            resolve();
          }, this.asyncTimeout);
         });
      }
    }

    .


  4. fake timers.
    Sinon.JS , . - ( ), setTimeout .
    :


    export class Foo {
      propToTest: boolean = false;
      methodToTest(): void {
        setTimeout(() => {
          this.propToTest = true;
        }, 100);
      }
    }

    fake timer, (!):


    let clock;
    
    beforeEach(() => {
      clock = sinon.useFakeTimers();
    });
    
    afterEach(() => {
      clock.restore();
    });
    
    it('should set propToTest to true', () => {
      const inst = newFoo();
      inst.methodToTest();
      clock.tick(101);
      assert.isTrue(inst.propToTest);
    });

    " " clock.restore(), .




- , . - , . , , .


All Articles