使用 webworker 测试 q.defer(),如何防止计时问题?
Testing q.defer() with a webworker, how do I prevent timing issues?
我有一个网络工作者为我做一些工作。
我已经将它包装到一个服务中,并且这个 webworker 在 Promise 中执行。
现在我正在用 Jasmine 测试这个,测试完成后似乎返回了 promised。
这里的困难在于 defer 和 webworker 在不同的时间点都是异步的。
我已经尝试使用带有 done、setTimeout、$scope.$apply() 的异步 jasmine。但是在所有这些计时器暂停后调用“'deferred.resolve(e.data.filtered)'”。
我的angular服务是这样的:
'use strict';
angular.module('app.demographics').service('FilteringService', FilteringService);
FilteringService.$inject = ['$q'];
function FilteringService($q) {
this.filter = function (dataSet, filters) {
var deferred = $q.defer();
var worker = new Worker('my.worker.js');
var filterData = {
dataSet: dataSet,
filters: filters
};
worker.postMessage(filterData);
worker.onmessage = function (e) {
if (e.data && e.data.ready) {
deferred.resolve(e.data.filtered);
}
};
worker.onerror = function (e) {
console.log("something went wrong while filtering: ", e);
deferred.reject(e);
};
return deferred.promise;
};
}
而我的测试是这样的,我希望它能正常工作,但它从来没有达到预期。
'use strict';
describe('FilteringService: ', function () {
var filteringService, $q,
dataSet = [{a: 1, b: 2}, {c: 3, d: 4}],
filters = [];
beforeEach(function () {
module('app.demographics');
inject(function (_$rootScope_, _FilteringService_, _$q_) {
filteringService = _FilteringService_;
$q = _$q_;
});
});
it('should return a promise on filtering', function () {
var filteringPromise = filteringService.filter(dataSet, filters);
filteringPromise.then(function (data) {
expect(data.length).toEqual(dataSet.length);
}, function (failure) {
fail(failure);
});
});
});
我承认这不是最佳解决方案,可能更像是一种 hack,但这就是我让 Jasmine 使用 Angular 的方式。我的方法是创建一个函数 digestIt,它采用 Jasmine 提供的 done 函数,并使用 setInterval 和 returns 调用 $digest 进行清理功能。
function digestIt($rootScope, done) {
var intervalId: number,
_done = function() {
if (angular.isDefined(intervalId))
clearInterval(intervalId);
intervalId = null;
done();
},
_interval = function () {
if (angular.isNumber(intervalId)) {
try {
$rootScope.$digest();
} catch (e) {
_done();
}
}
},
intervalId = setInterval(_interval, 1);
return _done;
}
这是使用模式。
describe("MyService ", function() {
var $rootScope,
$injector
;
beforeEach(angular.mock.inject(function (_$rootScope_, _$injector_) {
$rootScope = _$rootScope_.$new();
$injector = _$injector_;
}));
it("My Test", function (done) {
var $docs = $injector.get('MyService'),
completed = digestIt($rootScope, done)
;
$docs.asyncCall().then(function () {
/* expect */
}).catch(function() {
/* fail */
}).finally(function () {
completed();
});
});
});
看起来(至少在测试环境中),$q
promises 只有在启动摘要周期时才会得到解决(例如,它们的 success/failure 回调被调用)。所以在服务中你可以输入 $rootScope.apply()
来触发这个:
worker.onmessage = function (e) {
if (e.data && e.data.ready) {
$rootScope.$apply(function() {
deferred.resolve(e.data.filtered);
});
}
};
worker.onerror = function (e) {
console.log("something went wrong while filtering: ", e);
$rootScope.$apply(function() {
deferred.reject(e);
});
};
然后你的测试可以是异步的:
it('should return a promise on filtering', function (done) {
var filteringPromise = filteringService.filter(dataSet, filters);
filteringPromise.then(function (data) {
expect(data.length).toEqual(dataSet.length);
done();
}, function (failure) {
fail(failure);
});
});
这可以在https://plnkr.co/edit/D21EhoCXIbj8R0P9RY40?p=preview
看到
注意:这可能被归类为集成测试而不是单元测试,因为您同时测试 FilteringService
和您的工作人员。如果您只进行单元测试,则可以通过模拟工作人员来避免在 FilteringService
中添加 $rootScope.$apply()
。然后您可能还可以进行同步测试。
正如中提到的,原来的测试似乎更像是一个集成测试而不是单元测试。如果您希望这是一个单元测试....
您需要能够模拟 worker,这样您就不会测试它的功能。所以在服务中,你可以调用$window.Worker
而不是直接调用Worker
,因为$window
可以很容易地在测试中被模拟。
app.service('FilteringService', FilteringService);
FilteringService.$inject = ['$window', '$q', '$rootScope'];
function FilteringService($window, $q, $rootScope) {
this.filter = function (dataSet, filters) {
var deferred = $q.defer();
var worker = new $window.Worker('my.worker.js');
...
然后在测试中你可以创建一个模拟的 worker,调用真正的 worker 调用的 attacted onmessage
处理程序,并测试 promise 然后用正确的值解析(我已经让它只是测试长度,但在真正的测试中,我怀疑你需要更好的东西。
describe('FilteringService: ', function () {
var $rootScope, filteringService, $q,
dataSet = [{a: 1, b: 2}, {c: 3, d: 4}],
filters = [];
var mockWorker;
var mockWindow = {
Worker: function() {
return mockWorker;
}
};
beforeEach(function () {
module('app.demographics');
module(function($provide) {
$provide.value('$window', mockWindow);
});
inject(function (_$rootScope_, _FilteringService_, _$q_) {
$rootScope = _$rootScope_;
filteringService = _FilteringService_;
$q = _$q_;
});
mockWorker = {
postMessage: jasmine.createSpy('onMessage')
}
});
it('when onmessage from worker called, resolves returned promise with filtered list', function () {
expect(mockWorker.postMessage).not.toHaveBeenCalled();
expect(mockWorker.onmessage).not.toEqual(jasmine.any(Function));
var filteringPromise = filteringService.filter(dataSet, filters);
expect(mockWorker.postMessage).toHaveBeenCalled();
expect(mockWorker.onmessage).toEqual(jasmine.any(Function));
mockWorker.onmessage({
data: {
ready: true,
filtered: dataSet
}
});
var result;
filteringPromise.then(function(_result) {
result = _result;
});
$rootScope.$apply();
expect(result.length).toEqual(dataSet.length);
});
});
请注意,您随后需要 $apply
测试(但不是服务),以确保调用 promise 回调。
看到这个工作
我有一个网络工作者为我做一些工作。 我已经将它包装到一个服务中,并且这个 webworker 在 Promise 中执行。
现在我正在用 Jasmine 测试这个,测试完成后似乎返回了 promised。
这里的困难在于 defer 和 webworker 在不同的时间点都是异步的。
我已经尝试使用带有 done、setTimeout、$scope.$apply() 的异步 jasmine。但是在所有这些计时器暂停后调用“'deferred.resolve(e.data.filtered)'”。
我的angular服务是这样的:
'use strict';
angular.module('app.demographics').service('FilteringService', FilteringService);
FilteringService.$inject = ['$q'];
function FilteringService($q) {
this.filter = function (dataSet, filters) {
var deferred = $q.defer();
var worker = new Worker('my.worker.js');
var filterData = {
dataSet: dataSet,
filters: filters
};
worker.postMessage(filterData);
worker.onmessage = function (e) {
if (e.data && e.data.ready) {
deferred.resolve(e.data.filtered);
}
};
worker.onerror = function (e) {
console.log("something went wrong while filtering: ", e);
deferred.reject(e);
};
return deferred.promise;
};
}
而我的测试是这样的,我希望它能正常工作,但它从来没有达到预期。
'use strict';
describe('FilteringService: ', function () {
var filteringService, $q,
dataSet = [{a: 1, b: 2}, {c: 3, d: 4}],
filters = [];
beforeEach(function () {
module('app.demographics');
inject(function (_$rootScope_, _FilteringService_, _$q_) {
filteringService = _FilteringService_;
$q = _$q_;
});
});
it('should return a promise on filtering', function () {
var filteringPromise = filteringService.filter(dataSet, filters);
filteringPromise.then(function (data) {
expect(data.length).toEqual(dataSet.length);
}, function (failure) {
fail(failure);
});
});
});
我承认这不是最佳解决方案,可能更像是一种 hack,但这就是我让 Jasmine 使用 Angular 的方式。我的方法是创建一个函数 digestIt,它采用 Jasmine 提供的 done 函数,并使用 setInterval 和 returns 调用 $digest 进行清理功能。
function digestIt($rootScope, done) {
var intervalId: number,
_done = function() {
if (angular.isDefined(intervalId))
clearInterval(intervalId);
intervalId = null;
done();
},
_interval = function () {
if (angular.isNumber(intervalId)) {
try {
$rootScope.$digest();
} catch (e) {
_done();
}
}
},
intervalId = setInterval(_interval, 1);
return _done;
}
这是使用模式。
describe("MyService ", function() {
var $rootScope,
$injector
;
beforeEach(angular.mock.inject(function (_$rootScope_, _$injector_) {
$rootScope = _$rootScope_.$new();
$injector = _$injector_;
}));
it("My Test", function (done) {
var $docs = $injector.get('MyService'),
completed = digestIt($rootScope, done)
;
$docs.asyncCall().then(function () {
/* expect */
}).catch(function() {
/* fail */
}).finally(function () {
completed();
});
});
});
看起来(至少在测试环境中),$q
promises 只有在启动摘要周期时才会得到解决(例如,它们的 success/failure 回调被调用)。所以在服务中你可以输入 $rootScope.apply()
来触发这个:
worker.onmessage = function (e) {
if (e.data && e.data.ready) {
$rootScope.$apply(function() {
deferred.resolve(e.data.filtered);
});
}
};
worker.onerror = function (e) {
console.log("something went wrong while filtering: ", e);
$rootScope.$apply(function() {
deferred.reject(e);
});
};
然后你的测试可以是异步的:
it('should return a promise on filtering', function (done) {
var filteringPromise = filteringService.filter(dataSet, filters);
filteringPromise.then(function (data) {
expect(data.length).toEqual(dataSet.length);
done();
}, function (failure) {
fail(failure);
});
});
这可以在https://plnkr.co/edit/D21EhoCXIbj8R0P9RY40?p=preview
看到注意:这可能被归类为集成测试而不是单元测试,因为您同时测试 FilteringService
和您的工作人员。如果您只进行单元测试,则可以通过模拟工作人员来避免在 FilteringService
中添加 $rootScope.$apply()
。然后您可能还可以进行同步测试。
正如
您需要能够模拟 worker,这样您就不会测试它的功能。所以在服务中,你可以调用$window.Worker
而不是直接调用Worker
,因为$window
可以很容易地在测试中被模拟。
app.service('FilteringService', FilteringService);
FilteringService.$inject = ['$window', '$q', '$rootScope'];
function FilteringService($window, $q, $rootScope) {
this.filter = function (dataSet, filters) {
var deferred = $q.defer();
var worker = new $window.Worker('my.worker.js');
...
然后在测试中你可以创建一个模拟的 worker,调用真正的 worker 调用的 attacted onmessage
处理程序,并测试 promise 然后用正确的值解析(我已经让它只是测试长度,但在真正的测试中,我怀疑你需要更好的东西。
describe('FilteringService: ', function () {
var $rootScope, filteringService, $q,
dataSet = [{a: 1, b: 2}, {c: 3, d: 4}],
filters = [];
var mockWorker;
var mockWindow = {
Worker: function() {
return mockWorker;
}
};
beforeEach(function () {
module('app.demographics');
module(function($provide) {
$provide.value('$window', mockWindow);
});
inject(function (_$rootScope_, _FilteringService_, _$q_) {
$rootScope = _$rootScope_;
filteringService = _FilteringService_;
$q = _$q_;
});
mockWorker = {
postMessage: jasmine.createSpy('onMessage')
}
});
it('when onmessage from worker called, resolves returned promise with filtered list', function () {
expect(mockWorker.postMessage).not.toHaveBeenCalled();
expect(mockWorker.onmessage).not.toEqual(jasmine.any(Function));
var filteringPromise = filteringService.filter(dataSet, filters);
expect(mockWorker.postMessage).toHaveBeenCalled();
expect(mockWorker.onmessage).toEqual(jasmine.any(Function));
mockWorker.onmessage({
data: {
ready: true,
filtered: dataSet
}
});
var result;
filteringPromise.then(function(_result) {
result = _result;
});
$rootScope.$apply();
expect(result.length).toEqual(dataSet.length);
});
});
请注意,您随后需要 $apply
测试(但不是服务),以确保调用 promise 回调。