使用 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 回调。

您可以在 https://plnkr.co/edit/g2q3ZnD8AGZCkgkkEkdj?p=preview

看到这个工作