2

I've checked in How do I verify jQuery AJAX events with Jasmine? and How to test an Angular controller with a function that makes an AJAX call without mocking the AJAX call? but for some reason they don't necessarily come out with a format that works for me.

My issue is that I'm unit testing a function that makes a call to a function that fires off an AJAX request. I want to test the outer function without firing off the AJAX request (or halt that request) so I don't get a bunch of faulty data shot into the data server.

Here is my outer function (which calls the function containing the AJAX):

vm.clickSubmit = function () {
    vm.loading = true; // starts the loading spinner
    vm.updateData(); // makes current data current in the data model
    vm.inputData = {    // build the data to be submitted
        ...
    };
    vm.submit(); // calls the AJAX function
};

Here is my AJAX function:

vm.submit = function () {
    var d = $.ajax({
        type: "POST"
        , data: angular.toJson(vm.inputData)
        , url: "http://submit_the_data"
        , contentType: "application/json"
        , xhrFields: { withCredentials: true }
        , crossDomain: true
    })
    .success(function (resp) {
        DataService.ticketNumber = resp; // returns ticket# for the data model
    })
    .error(function (error) {
        DataService.ticketNumber = DataService.submitError;
    });
    d.then(function (d) {
        vm.loading = false; // stops the loading spinner
        DataService.tickets = []; // empty's the array to be filled anew
        $location.path('/submitted'); // success splash html
        $scope.$apply();
    });
};

I've written all the tests that will read and verify the values in the inputData object, but I'm not sure how to surround the call to clickSubmit() so nothing is actually submitted to the server. I've gotten to this point in my unit testing:

'use strict';

describe('Controller: HomeController', function () {
    beforeEach(module('tickets'));
    var controller, scope, $location, DataService;
    var tests = 0;
    beforeEach(inject(function ($rootScope, $controller, _$location_, _DataService_) {
        $location = _$location_;
        DataService = _DataService_;
        scope = $rootScope.$new();
        controller = $controller('HomeController', {
            $scope: scope
        });
    }));
    afterEach(function () {
        tests += 1;
    });
    describe('clickSubmit should verify data and submit new ticket', function () {
        beforeEach(function () {
            jasmine.Ajax.install();
            controller.loading = false;
                ... // inputData fields filled in with test data
        });
        afterEach(function () {
            jasmine.Ajax.uninstall();
        });
        it('should start the spinner when called', function () {
            controller.clickSubmit();
            expect(controller.loading).toBeTruthy();
        });
        // other it('') tests
    });
    it('should have tests', function () {
        expect(tests).toBeGreaterThan(0);
    });
});

So what should go after the expect on the loading spinner to cancel the call to vm.submit() in the actual code?

Thanks,

-C§

Community
  • 1
  • 1
CSS
  • 412
  • 5
  • 17
  • First of all i'd suggest you to move your ajax calls into a seperate service. Seems like you're only setting your data from the service calls into your angular service/factory right now. Then you should test the service/factory itself. Angular provides the $httpBackend (https://docs.angularjs.org/api/ngMock/service/$httpBackend) for that so you don't have to actually call the service. For the controller test you should mock the service/factory, so you don't have to care about what your service/factory does. – m.brand Aug 13 '15 at 07:48
  • @m.brand Would you mind filling in an answer below illustrating this method? I only ask because I'm still very new to Angular/Jasmine/Karma. Also, does $httpBackend work with the cited AJAX call or would I have to convert it to the $http? Thanks, C§ – CSS Aug 13 '15 at 15:14
  • This answer shows a couple of solutions for pulling out ajax calls into services: http://stackoverflow.com/questions/17646034/what-is-the-best-practice-for-making-an-ajax-call-in-angular-js – DavidA Aug 13 '15 at 16:09

1 Answers1

2

I suggest mocking out the call rather than actually calling it. The granularity is up to you, you can either stub the ajax call, or stub the whole submit function.

Here is how you can stub the submit function:

spyOn(controller, 'submit').and.callFake(function() {
    DataService.ticketNumber = somevalue;
});

Place that code prior to the actually call to the controller which caller.clickSubmit().

You can then follow up with expectations on the spy, such as:

expect(controller.submit).toHaveBeenCalled()

Or any of the other expectations related to spyOn.

Here are the jasmine docs: http://jasmine.github.io/2.0/introduction.html
Look down in the 'spies' area.

If you want to mock up the ajax call, you will have to mock a promise like this:

spyOn($, 'ajax').and.callFake(function() {
    var deferred = $q.defer();
    deferred.resolve(someResponse);
    return deferred.promise;
});

Also, in order to get the code waiting on the promise to resolve, after the submit call has been made, you need to run a $scope.$digest() so that angular can handle the promise resolution. Then you can check your expectations on code that depended on the resolution or rejection of the promise.

DavidA
  • 3,984
  • 5
  • 25
  • 38
  • Got an error on my `updateData()` function "Error: Expected a spy, but got function.". I put the `spyOn` block in the `beforeEach` block, after the loading inputData block. It's the only test in the set that failed. The `expect(controller.submit).toHaveBeenCalled();` passed, so I'm confused why the `updateData()` test failed. – CSS Aug 12 '15 at 22:26
  • 1
    Never mind, got it to work using `spyOn(controller, 'updateData').and.callThrough();`. Thanks for the help. I'll likely refer to this frequently when mocking up the data service (which now has all 3 AJAX calls in it since I moved this one out of the controller). - C§ – CSS Aug 13 '15 at 15:25
  • Solved my own issue. I was getting the error "$q is not defined" and realized I just need to inject it like so: `beforeEach(inject(function($q){ ... }));` I really appreciate what you've offered me here. Thanks, C§ – CSS Aug 14 '15 at 20:54