Search code examples
angularjsjasmineweb-workerdeferred

Testing q.defer() with a webworker, how do I prevent timing issues?


I have a webworker doing some work for me. I've wrapped this into a service and this webworker is executed in a Promise.

Now I'm tesing this with Jasmine, and it seems that the promised is returned after the test has finished.

The difficulty in here is that the defer AND the webworker are both async at different points in time.

I've tried with async jasmine with done, setTimeout, $scope.$apply(). But ''deferred.resolve(e.data.filtered)'' is called after all those timers have suspended.

My angular service is like this:

'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;
    };
}

And my test is like this, which I expect to work properly, but it never comes to the expect.

'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);
        });
    });
});

Solution

  • As mentioned in https://stackoverflow.com/a/37853075/1319998, the original test seems to be more of an integration test rather than a unit test. If you would like this to be a unit test....


    You need to be able to mock the worker so you're not testing what it does. So in the service, instead of calling Worker directly, you can call $window.Worker, since $window can be easily mocked in tests.

    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');
        ...
    

    Then in the test you can create a mocked worker, calling the attacted onmessage handler that would be called by the real worker, and testing that the promise then gets resolved with the correct value (I've left it as just testing the length, but in a real test I suspect you will need something a bit better).

    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);
      });
    });
    

    Note you then need the $apply in the test (but not the service), to make sure the promise callbacks get called.

    You can see this working at https://plnkr.co/edit/g2q3ZnD8AGZCkgkkEkdj?p=preview