【问题标题】:Unit-test promise-based code in AngularjsAngularjs 中基于 Promise 的单元测试代码
【发布时间】:2018-03-29 13:46:39
【问题描述】:

我很难在 Angularjs 中测试基于 Promise 的代码。

我的控制器中有以下代码:

    $scope.markAsDone = function(taskId) {
        tasksService.removeAndGetNext(taskId).then(function(nextTask) {
            goTo(nextTask);
        })
    };

    function goTo(nextTask) {
        $location.path(...);
    }

我想对以下案例进行单元测试:

  • markAsDone被调用时,它应该调用tasksService.removeAndGetNext
  • tasksService.removeAndGetNext 完成后,它应该改变位置(调用goTo

在我看来,单独测试这两种情况并不容易。

我测试第一个是:

var noopPromise= {then: function() {}}
spyOn(tasksService, 'removeAndGetNext').andReturn(noopPromise);

现在要测试第二种情况,我需要创建另一个始终为resolved 的虚假承诺。这一切都很乏味,而且有很多样板代码。

还有其他方法可以测试这些东西吗?还是我的设计有异味?

【问题讨论】:

标签: javascript angularjs promise


【解决方案1】:

您仍然需要模拟服务并返回一个 Promise,但您应该使用真正的 Promise,因此您不需要实现它的功能。使用beforeEach 创建已经实现的承诺并模拟服务,如果您需要它总是得到解决。

var $rootScope;

beforeEach(inject(function(_$rootScope_, $q) {
  $rootScope = _$rootScope_;

  var deferred = $q.defer();
  deferred.resolve('somevalue'); //  always resolved, you can do it from your spec

  // jasmine 2.0
  spyOn(tasksService, 'removeAndGetNext').and.returnValue(deferred.promise); 

  // jasmine 1.3
  //spyOn(tasksService, 'removeAndGetNext').andReturn(deferred.promise); 

}));

如果您希望在每个 it 块中使用不同的值来解析它,那么您只需将 deferred 暴露给一个局部变量并在规范中解析它。

当然,您可以保持测试原样,但这里有一些非常简单的规范来向您展示它是如何工作的。

it ('should test receive the fulfilled promise', function() {
  var result;

  tasksService.removeAndGetNext().then(function(returnFromPromise) {
    result = returnFromPromise;
  });

  $rootScope.$apply(); // promises are resolved/dispatched only on next $digest cycle
  expect(result).toBe('somevalue');
});

【讨论】:

  • 你能解释一下 $rootScope = $rootScope; 的用途吗?在你的第一个例子中?
  • 我正在做的是注入$rootScope(如果您在前后插入一个_,它将被忽略)并通过局部变量公开它。这样你就不需要将它注入到每个单独的规范中,因为它们都可能会使用它。
  • +1 我错过了这个 $rootScope.$apply() - 非常有帮助,谢谢
  • @CaioToOn 有什么方法可以在不应用范围的情况下发送承诺吗?
  • 备案,整个var deferred = $q.defer(); deferred.resolve('something'); returnValue(deferred.promise)可以简化为returnValue($q.when('something'))
【解决方案2】:

另一种方法如下,直接取自我正在测试的控制器:

var create_mock_promise_resolves = function (data) {
    return { then: function (resolve) { return resolve(data); };
};

var create_mock_promise_rejects = function (data) {
    return { then: function (resolve, reject) { if (typeof reject === 'function') { return resolve(data); } };
};

var create_noop_promise = function (data) {
    return { then: function () { return this; } };
};

【讨论】:

  • 你的 promise mock 将同步运行 resolve() 或 reject() 函数,但在 AngularJs 中,它总是异步完成。所以我不会推荐这个解决方案,因为它不能准确地测试代码。
  • 在单元测试中,您希望能够同步运行异步代码。这就是为什么像 $httpBackend 这样的服务会在 Angular 测试环境中自动模拟出来。
  • 其实我完全明白你的意思。这些承诺会自动刷新。您希望能够根据命令刷新它们。
【解决方案3】:

作为另一种选择,您可以通过使用 Q 库 (https://github.com/kriskowal/q) 作为 $q 的直接替代品来避免调用 $digest,例如:

beforeEach(function () {
    module('Module', function ($provide) {
        $provide.value('$q', Q); 
    });
});

这种方式可以在 $digest 循环之外解决/拒绝承诺。

【讨论】:

    猜你喜欢
    • 2015-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-11
    • 2016-01-29
    • 1970-01-01
    相关资源
    最近更新 更多