Testing an AngularJS factory that returns a promise and mocking a service that utilizes $http

My service includes a method that returns an $http promise:

  function sessionService($http, serviceRoot) {
    return {
        getAvailableDates: function () {
            return $http.get(serviceRoot + '/session/available_dates');
        }
    };
  };

  angular.module('app').service('sessionService', ['$http', 'serviceRoot', sessionService]);

Additionally, I have another factory that wraps it and manages data stored in localStorage. This factory returns a regular promise:

angular.module('app')
    .factory('AvailableDates', AvailableDates);

AvailableDates.$inject = ['sessionService', '$window', '$q'];

function AvailableDates(sessionService, $window, $q) {
    var availableDates = [];

    return {
        getAvailableDates: getAvailableDates
    };

    function getAvailableDates() {
        var deferred = $q.defer();
        var fromStorage = JSON.parse($window.sessionStorage.getItem('validDates'));

        if (availableDates.length > 0) {
            deferred.resolve(availableDates);
        } else if (fromStorage !== null) {
            deferred.resolve(fromStorage);
        } else {
            sessionService.getAvailableDates()
                .success(function (result) {
                    availableDates = result;
                    $window.sessionStorage.setItem('validDates', JSON.stringify(availableDates));
                    deferred.resolve(availableDates);
                });
        }
        return deferred.promise;
    }
}

Although this setup works fine, I'm struggling to test it while mocking the sessionService. Despite reading numerous resources and attempting various methods, I haven't been successful.

Below is my current testing approach:

describe('testing AvailableDates factory', function () {
    var mock, service, rootScope, spy, window, sessionStorageSpy, $q;
    var dates = [ "2014-09-27", "2014-09-20", "2014-09-13", "2014-09-06", "2014-08-30" ];
    var result;

    beforeEach(module('app'));

    beforeEach(function() {
        return angular.mock.inject(function (_sessionService_, _AvailableDates_, _$rootScope_, _$window_, _$q_) {
            mock = _sessionService_;
            service = _AvailableDates_;
            rootScope = _$rootScope_;
            window = _$window_;
            $q = _$q_;
        });
    });

    beforeEach(inject(function () {
        // my service under test calls this service method
        spy = spyOn(mock, 'getAvailableDates').and.callFake(function () {
            return {
                success: function () {
                    return [ "2014-09-27", "2014-09-20", "2014-09-13", "2014-09-06", "2014-08-30" ];
                },
                error: function() {
                    return "error";
                }
            };
        });

        spyOn(window.sessionStorage, "getItem").and.callThrough();
    }));

    beforeEach(function() {
        service.getAvailableDates().then(function(data) {
            result = data;
            // Should I use done() here??
        });
    });

    it('first call to fetch available dates hits sessionService and returns dates from the service', function () {
        rootScope.$apply(); // Any corrections needed here??

        console.log(result); // Outputting as undefined

        expect(spy).toHaveBeenCalled();  // Test passes
        expect(window.sessionStorage.getItem).toHaveBeenCalled(); // Test passes
    });
});

I have attempted different approaches without success in testing the result of AvailableDates.getAvailableDates() call. When using done(), I encounter a timeout error - "Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL". If I exclude done() and directly call rootScope.$apply() after the .then is executed, the result remains undefined.

Any insights on what could be causing this issue?

Answer №1

Your example presents several issues that can be addressed.

The primary concern lies in the definition of success within the mock. Success is depicted as a function with another function - callback - as a parameter. This callback is triggered upon receiving data, with the data being passed as the initial argument.

return {
    success: function (callback) {
        callback(dates);
    }
};

A simplified working instance can be found at http://plnkr.co/edit/Tj2TZDWPkzjYhsuSM0u3?p=preview

In this scenario, the mock is supplied to the provider using the module function from ngMock. By providing an object with a key (service name) and value (implementation), that implementation will be utilized for injection.

module({
      sessionService:sessionServiceMock
});

I believe that test logic should be consolidated into one function (test) rather than splitting it between beforeEach and test sections. A unified test structure like my example enhances readability and clearly separates the parts - arranging, acting, and asserting.

inject(function (AvailableDates) {
    AvailableDates.getAvailableDates().then(function(data) {
      expect(data).toEqual(dates);
      done();
    });

    rootScope.$apply(); // promises are resolved/dispatched only on next $digest cycle

    expect(sessionServiceMock.getAvailableDates).toHaveBeenCalled();
    expect(window.sessionStorage.getItem).toHaveBeenCalled();
  });

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

"Issues with Angular ng-show Functionality on Internet Explorer Versions 7 and

I'm currently working on a feature where I need to show or hide two divs conditionally using ng-show based on the completion of an AJAX call. The layout for this includes: <div id="div1" ng-show="!loadingData"> <!--Some markup here--> ...

Issue with button style in dropdown menu in Twitter Bootstrap

<nav class="nav-collapse user"> <div class="user-info pull-right"> <img src="http://placekitten.com/35/35" alt="User avatar"> <div class="btn-group"> ...

Identify all elements that possess a specific attribute with a precise value in jQuery, and return either true or false

I'm having a slight issue with jQuery. Below is the code in question: if ($("select[rendelo='" + rendelo + "'][nap='" + nap + "'][napszak='" + napszak + "']").val() == 0) { alert('sth'); ...

How can the node version be set globally in NVM?

While utilizing Node Version Manager, setting the node version to the latest one in the current directory can be done using nvm use node. But how can you specify a specific version to use? ...

I am currently working on implementing data filtering in my project. The data is being passed from a child component to a parent component as checkboxes. Can anyone guide me on how to achieve this filtering process

Below is the code I have written to filter data based on priorities (low, medium, high). The variable priorityArr is used to store the filtered data obtained from "this.data". The following code snippet is from the parent component, where "prio" is the v ...

What's the best way to animate the navigation on top of an image for movement?

I am currently in the process of creating my website as a graphic designer. My unique touch is having the navigation positioned on top of an image (which is animated via flash). This setup is featured prominently on my homepage, which is designed with mini ...

Loading an HTML page inside a tab's content in Angular: A seamless and efficient approach

There are multiple tabs within the master.html page, each loading a different tab*.html page with AngularJS code. The problem arises when I try to load the tab*.html pages separately; they work fine. Here is a Plunker example for a single page: Plunker fo ...

Creating an interactive HTML form that updates in real-time based on user input can be achieved using vanilla JavaScript. This allows for a

I am working on a form that dynamically generates more form fields based on a user input value. <form> <input type="Number" name="delivery"> </form> For example, if the user enters '3', it should automat ...

Utilize a WebAPI controller to serialize a complicated JSON object

Embarking on a new journey with AngularJS and ASP.NET WebAPI, I find myself faced with the challenge of working with base tables structured as follows: CurriculumID SubjectArea CourseNumber ------------ ----------- ------------ 303 GHIJ 1 ...

Generate an array consisting of characters within a designated range

I recently came across some Ruby code that caught my attention: puts ('A'..'Z').to_a.join(',') The output was: A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z I'm curious if there is a similar way to achieve this ...

Puppeteer throwing an error when querying selectors cannot be done (TypeError: selector.startsWith is not a supported function)

I recently installed Puppeteer and ran into an issue when trying to query a selector. I keep receiving a TypeError: selector.startsWith is not a function error. I attempted to resolve the problem by tweaking the core code and adding toString(), but unfort ...

What is the best way to configure input fields as readonly, except for the one being actively filled by the user

Is there a way to make all input fields readonly except the one that the user is trying to fill data into? After the user loads the page index.php and attempts to input data into, for example, <input id="edValue2" ...>, I want to set all input field ...

jquery failing to interpret ajax response

After making changes to fix an issue with an ajax request not working in IE, I am now facing a new problem where it is not working in any browser. Surprisingly, the ajax request does receive a proper result, but the result is not being parsed correctly. Ho ...

Error message: "When using Webpack 4 with Bootstrap, a TypeError occurs where property 'jquery' is read as undefined."

I've recently delved into the world of webpack and everything seems to be running smoothly. However, when I run my webpack app, I encounter the following error within Bootstrap: var version = $.fn.jquery.split(' ')[0].split('.'); ...

A little brain teaser for you: Why is this not functioning properly on Google Chrome?

Have you noticed that the code below works perfectly in Firefox, but fails in Chrome when trying to 'post' data? $("a").click(function() { $.post("ajax/update_count.php", {site: 'http://mysite.com'}); }); Hint: Make sure you are us ...

Dealing with multiple ajax requests while utilizing a single fail callback

Looking for a solution: I have two arrays containing deferreds. In order to detect failures in the outer or inner 'when' statements, I currently need to use double 'fail' callbacks. Is there a way to consolidate errors from the inner &a ...

Javascript/jquery functions perfectly in all browsers except Firefox

This particular piece of code seems to be functioning properly in Internet Explorer 8, Chrome, and Safari, however, it is not working as expected in Firefox: <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></scr ...

What could be the reason for my jQuery call displaying as undefined?

How can I extract a URL from an <a> tag and show the first paragraph of the linked page below the title? At the moment, it just shows 'undefined'. Here's my HTML: <section class="community"> <div class="news"> <ul cla ...

What could be the reason my node.js application built with express is unable to retrieve data from mongoose

Currently, I have experience in PHP MVC and recently delved into learning Nodejs. Here is how my app directory structure looks like: root - controllers -user.js - model -user.js - public -stylesh ...

How can I identify when a form has been edited in order to update the database with those changes only?

Currently, I have a form with over 50 fields. While I've successfully implemented functionality for inserting, selecting, and deleting records, I'm struggling with updating a record. Specifically, if a user only updates one or two fields out of t ...