【问题标题】:Unit Testing Angular Service that uses $timeout with Jasmine's Mock Clock使用 $timeout 和 Jasmine 的模拟时钟的单元测试 Angular 服务
【发布时间】:2013-07-02 06:21:14
【问题描述】:

我的一个 Angular 服务中有一个函数,我希望定期重复调用该函数。我想使用 $timeout 来做到这一点。它看起来像这样:

var interval = 1000; // Or something

var _tick = function () {
     $timeout(function () {
        doStuff();
        _tick();
    }, interval);
};

_tick();

目前,我对如何使用 Jasmine 进行单元测试感到困惑 - 我该怎么做?如果我使用$timeout.flush(),那么函数调用会无限期地发生。如果我使用 Jasmine 的模拟时钟,$timeout 似乎不受影响。基本上,如果我能做到这一点,我应该很高兴:

describe("ANGULAR Manually ticking the Jasmine Mock Clock", function() {
    var timerCallback, $timeout;

    beforeEach(inject(function($injector) {
        $timeout = $injector.get('$timeout');
        timerCallback = jasmine.createSpy('timerCallback');
        jasmine.Clock.useMock();
    }));

    it("causes a timeout to be called synchronously", function() {
        $timeout(function() {
            timerCallback();
        }, 100);
        expect(timerCallback).not.toHaveBeenCalled();
        jasmine.Clock.tick(101);
        expect(timerCallback).toHaveBeenCalled();
    });
});

这两种变体有效,但对我没有帮助:

describe("Manually ticking the Jasmine Mock Clock", function() {
    var timerCallback;

    beforeEach(function() {
        timerCallback = jasmine.createSpy('timerCallback');
        jasmine.Clock.useMock();
    });

    it("causes a timeout to be called synchronously", function() {
        setTimeout(function() {
            timerCallback();
        }, 100);
        expect(timerCallback).not.toHaveBeenCalled();
        jasmine.Clock.tick(101);
        expect(timerCallback).toHaveBeenCalled();
    });
});

describe("ANGULAR Manually flushing $timeout", function() {
    var timerCallback, $timeout;

    beforeEach(inject(function($injector) {
        $timeout = $injector.get('$timeout');
        timerCallback = jasmine.createSpy('timerCallback');
    }));

    it("causes a timeout to be called synchronously", function() {
        $timeout(function() {
            timerCallback();
        }, 100);
        expect(timerCallback).not.toHaveBeenCalled();
        $timeout.flush();
        expect(timerCallback).toHaveBeenCalled();
    });
});

提前致谢!

【问题讨论】:

  • 尝试注入$rootScope并在将时钟向前推后调用$rootScope.$apply()

标签: javascript angularjs jasmine


【解决方案1】:

不要使用 Jasmine 的时钟使您的测试异步。相反,使用$timeout.flush() 来同步维护测试流程。设置起来可能有点棘手,但一旦你得到它,你的测试会更快,更可控。

下面是一个使用这种方法进行测试的示例: https://github.com/angular/angular.js/blob/master/test/ngAnimate/animateSpec.js#L618

【讨论】:

  • 您的回答解决了问题,并重申了一条重要的经验法则:单元测试应该始终是同步的
【解决方案2】:

@matsko 的回答让我走上了正确的道路。我想我会发布我的“完整”解决方案,以便更轻松地找到答案。

要测试的东西

angular.module("app").service("MyService", function() {
    return {
        methodThatHasTimeoutAndReturnsAPromise: function($q, $timeout) {
            var deferred = $q.defer();
            $timeout(function() {
                deferred.resolve(5);
            }, 2000);
            return deferred.promise;
        }
    };
});

测试

describe("MyService", function() {
    var target,
        $timeout;
    beforeEach(inject(function(_$timeout_, MyService) {
        $timeout = _$timeout_;
        target = MyService;
    }));
    beforeEach(function(done) {
        done();
    });
    it("equals 5", function(done) {
        target.methodThatHasTimeoutAndReturnsAPromise().then(function(value) {
            expect(value).toBe(5);
            done();
        });
        $timeout.flush();
    });
});

【讨论】:

  • “等于 5”测试中的done() 不是多余的吗? $timeout.flush() 将同步调用 $timeout 中注册的所有未决事件,这将导致 Promise 解决,这将立即调用 expect()
  • 我不完全确定。不过可能值得一试。
  • @AviCherry 如果没有done 回调参数,代码将触发刷新但立即认为测试通过,因为它不知道触发了异步调用。使用 done ,您可以确保在调用 done 之前不会完成测试。 expect 不会完成测试。您可以在测试中有多个期望。此外,如果你提供了done 回调但你从未执行它,你的测试将失败,因为 Jasmine 会抛出类似execution took too long 的东西,因为测试永远不会完成。不记得确切的反馈。
  • 啊,是的。这是正确的。因此,请注意调用done() firstbeforeEach 函数。这是告诉 Jasmine 一个异步调用即将到来的关键。因此,当$timeout.flush() 被调用时,它会一直挂起,直到在您完成expect-ing 之后调用done()。好消息@Justus!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多