Mahdi
Mahdi

Reputation: 755

Integration test using AngularJS $http and Jasmine

I have an AngularJS service that calls server via $http like this

function DefineCourseService($http) {
  var service = {
    getCourses: getCourses
  };

  function getCourses(id) {
    return $http({
      url: '/api/Course',
      method: 'GET'
    });
  }
}

and the server returns :

[{Code:'123',Title:'Test'}]

I want to write an integration test using Jasmine that gets response from server and checks its value. The test file is like:

(function() {
  'use strict';
  define(['angular-mocks', 'defineCourse.service'], function() {
    describe("Course service", function() {
      var courseService, data, deferredResolution, parentScope;

      beforeEach(function() {
        module('modabber.services');

      });

      beforeEach(inject(function($q, $rootScope, DefineCourseService) {
        courseService = DefineCourseService;
        deferredResolution = $q.defer();
        parentScope = $rootScope;
      }));

      it("get courses", function() {
        spyOn(courseService, 'getCourses').and.callThrough();
        deferredResolution.resolve();
        courseService.getCourses().then(function(result) {
          data = result;
        });
        expect(courseService.getCourses).toHaveBeenCalled();
        expect(data).toBeUndefined();

        parentScope.$digest();
        expect(data).toBeDefined();
        done();
      });
    });
  });
})();

and at last my karma.conf.js:

module.exports = function(config) {
  config.set({
    basePath: '../',
    frameworks: ['jasmine', 'requirejs'],
    files: [
      'karma/test-main.js', {
        pattern: 'WebApiControllers/**/*.js',
        included: false
      }, {
        pattern: 'scripts/vendor/*.js',
        included: false
      }, {
        pattern: 'bower_components/ngMidwayTester/src/ngMidwayTester.js',
        included: false
      }, {
        pattern: 'bower_components/**/*.min.js',
        included: false
      }, {
        pattern: 'scripts/*.js',
        included: false
      }, {
        pattern: 'app/services/*.js',
        included: false
      }, {
        pattern: 'app/directives/*.js',
        included: false
      },

    ],
    exclude: ['scripts/main.js'],
    preprocessors: {    },
    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false
  });
}

but it always fails as "data" is undefined so what's the problem?

Upvotes: 1

Views: 1411

Answers (2)

Lawrence Choy
Lawrence Choy

Reputation: 6108

angular-mock will not make real ajax calls because it can make unit tests inaccurate. In order to let Angular call the web service and still be able to maintain a good test, I recommend to use ngMidwayTester. Combining it with Jasmine's async support (done() function), you can perform your test.

describe('Course service', function() {
    var tester;
    var courseService;

    beforeEach(function() {
        tester = ngMidwayTester('modabber.services');
        courseService = tester.inject('DefineCourseService');
    });

    afterEach(function() {
        tester.destroy();
        tester = null;
    });

    it('get courses', function(done) {
        courseService.getCourses()
            .then(function(result) {
                expect(result).toBeDefined();
                expect(result.Code).toBe('123');
                done();
            }, function() {
                done.fail('Web service call failed!');
            });
    });
});

Upvotes: 6

Mario Levrero
Mario Levrero

Reputation: 3367

Explanation

You are setting data inside your async callback function, but evaluating it outside. Imagine your async call takes 3 seconds... This would be its lifecycle:

  1. getCourses is called
  2. data is undefined and evaluation fails
  3. your test finishes
  4. ... after 3 seconds... getCourses callback and set it to new value

Proposal

If you want to test an asyncrhonous call with jasmine as you have in courseService.getCourses() you need to notice it to your test using the parameter done.

From jasmine documentation - Asynchronous support:

This spec will not start until the done function is called in the call to beforeEach above. And this spec will not complete until its done is called.

So basic implementation, as you can see at docu, is:

it("takes a long time", function(done) { //done as a parameter
      setTimeout(function() {     
        done();                           //done is resolve to finish your test
      }, 9000);
    });

In addition, your expects need to be evaluate after receiving the callback from the async, so introduce them in your then callback function. So, assuming your service injection is correct, you should have something like:

it("get courses", function(done) { //Add done as parameter
        // spyOn(courseService, 'getCourses').and.callThrough();Now this spyOn makes no sense.
        deferredResolution.resolve();
        courseService.getCourses().then(function(result) {
          data = result;
          // expect(courseService.getCourses).toHaveBeenCalled(); Not needed
          // expect(data).toBeUndefined();  I assume you are expecting it defined
          // parentScope.$digest(); If you are only evaluating result and it's not change by the $scope, you don't need a $digest.
          expect(data).toBeDefined();
          done();
        }, function(){
           //If your async call fails and done() is never called, 
           // your test will fail with its timeout...
           //... Or you can force a test error
           expect(1).toBe(2);
           done();
        });

      });

Upvotes: 2

Related Questions