Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS - Unit testing file uploads

As you know, inside unit tests it's built-in angularjs feature to mock XHR requests with $httpBackend - this is nice and helpful while writing unit tests.

Recently, I met with need of mocking XHR in case of file upload and discovered some problems.

Consider following code:

var xhr = new XMLHttpRequest();

xhr.upload.addEventListener("progress", uploadProgress(event), false);
xhr.addEventListener("load", uploadComplete(event), false);
xhr.addEventListener("error", uploadError(event), false);
xhr.addEventListener("abort", uploadAbort(event), false);
xhr.open("POST", 'some url');
xhr.send(someData);

What I want to do is to do unit testing of such a code with mocking of XHR requests, but it's not possible do it because there is no $http service used here.

I tried this (and it's working and could be mocked with $httpBackend):

$http({
    method: 'POST', 
    url: 'some url', 
    data: someData, 
    headers: {'Content-Type': undefined},
    transformRequest: angular.identity})
.then(successCallback, errorCallback);

But in this case I don't know how to implement 'progress' callback and 'abort' callback (they are essential and required in case I am working on now).

I've seen information that latest Angular supports progress callback for promises (not sure though whether it's integrated with $http service), but what about abort callback?

Any ideas or maybe your met with something similar before?

like image 476
Egor Smirnov Avatar asked Sep 25 '13 10:09

Egor Smirnov


2 Answers

If the $http service doesn't give you everything you need, you can still unit test the first block of code. First of all, change your code to use Angular's $window service. This is just a wrapper service, but it allows you to mock the object in your tests. So, you'll want to do this:

var xhr = new $window.XMLHttpRequest();

Then in your tests, just mock it and use spies.

$window.XMLHttpRequest= angular.noop;
addEventListenerSpy = jasmine.createSpy("addEventListener");
openSpy = jasmine.createSpy("open");
sendSpy = jasmine.createSpy("send");
xhrObj = {
   upload: 
   {
       addEventListener: addEventListenerSpy
   },
   addEventListener: addEventListenerSpy,
   open: openSpy,
   send: sendSpy
};
spyOn($window, "XMLHttpRequest").andReturn(xhrObj);

From there, you can make the different spies return whatever you want for the different tests.

like image 174
dnc253 Avatar answered Sep 18 '22 23:09

dnc253


You should mock $http and control any deferred, as you want more control over your test. Basically, mock $http provider and serve a custom implementation that exposes its deferred, then play with it.

You should not worry whether $http is working right or not, because it is supposed to, and is already tested. So you have to mock it and only worry testing your part of the code.

You should go something like this:

describe('Testing a Hello World controller', function() {
  beforeEach(module(function($provide) {
    $provide.provider('$http', function() {
      this.$get = function($q) {
        return function() {
          var deferred = $q.defer(),
              promise = deferred.promise;

          promise.$$deferred = deferred;
          return promise;
        }
      };
    });
  }));

  it('should answer to fail callback', inject(function(yourService, $rootScope) {
    var spyOk = jasmine.createSpy('okListener'),
        spyAbort = jasmine.createSpy('abortListener'),
        spyProgress = jasmine.createSpy('progressListener');

    var promise = yourService.upload('a-file');
    promise.then(spyOk, spyAbort, spyProgress);

    promise.$$deferred.reject('something went wrong');
    $rootScope.$apply();

    expect(spyAbort).toHaveBeenCalledWith('something went wrong');
  }));
});

And your service is simply:

app.service('yourService', function($http) {
  return {
    upload: function(file) {
      // do something and
      return $http({...});
    }
  };
});

Just note that promises notification is only available in the latest RC release. So, if you can't use it, just elaborate a little more the example and mock the XHR events and so.

Also note that you should preferably have one test case for each of the callbacks (fail, success and progress), in order to follow KISS principle.

like image 21
Caio Cunha Avatar answered Sep 18 '22 23:09

Caio Cunha