2

I'm currently going through the book called 'Mastering Web Application Development with AngularJS' and in one of the examples there is a test called 'Aggregating callbacks'.

The example I've got the problem with contains the Person object:

var Person = function(name, $log) {

    this.eat = function(food) {

        $log.info(name + ' is eating delicious ' + food);

    };

    this.beHungry = function(reason) {

        $log.warn(name + ' is hungry because: ' + reason);

    };

};

The object called Restaurant:

var Restaurant = function($q, $rootScope) {

    var currentOrder;

    return {

        takeOrder : function(orderedItems) {

            currentOrder = {

                deferred : $q.defer(),
                items : orderedItems

            };

            return currentOrder.deferred.promise;

        },

        deliverOrder : function() {

            currentOrder.deferred.resolve(currentOrder.items);
            $rootScope.$digest();

        },

        problemWithOrder : function(reason) {

            currentOrder.deferred.reject(reason);
            $rootScope.$digest();

        }

    };

};

and lastly the test for the aggregating callback:

it('should allow callbacks aggregation', function() {

    var pizzaPid = new Restaurant($q, $rootScope);

    var pizzaDelivered = pizzaPid.takeOrder('Margherita');

    pizzaDelivered.then(pawel.eat, pawel.beHungry);
    pizzaDelivered.then(pete.eat, pete.beHungry);

    pizzaPid.deliveryOrder();

    expect($log.info.logs).toContain(['Pawel is eating delicious Margherita']);
    expect($log.info.logs).toContain(['Pete is eating delicious Margherita']);

});

As you can see the test doesn't show how items are added / injected to the test and I'm new to the concept of TDD in general.

What I've ended up doing was to convert those global objects to service and factory:

angular.module('myApp', [])

    .service('Person', function(personName, $log) {

        this.eat = function(food) {

            $log.info(personName + ' is eating delicious ' + food);

        };

        this.beHungry = function(reason) {

            $log.warn(personName + ' is hungry because: ' + reason);

        };

    })

    .factory('Restaurant', function($q, $rootScope) {

        var currentOrder;

        return {

            takeOrder : function(orderedItems) {

                currentOrder = {

                    deferred : $q.defer(),
                    items : orderedItems

                };

                return currentOrder.deferred.promise;

            },

            deliverOrder : function() {

                currentOrder.deferred.resolve(currentOrder.items);
                $rootScope.$digest();

            },

            problemWithOrder : function(reason) {

                currentOrder.deferred.reject(reason);
                $rootScope.$digest();

            }

        };

    });

But now I'm struggling with the multiple instances of the service to represent 'pawel' and 'pete' in my test:

describe('Person and Restaurant tests', function() {

    var Person;
    var Restaurant;

    var $q;
    var $rootScope;
    var $log;




    beforeEach(function() {


        module('myApp');

        module(function($provide) {

            $provide.value('personName', 'Pawel');

        });

        inject(function(_Person_, _Restaurant_, _$q_, _$rootScope_, _$log_) {

            Person = _Person_;
            Restaurant = _Restaurant_;

            $q = _$q_;
            $rootScope = _$rootScope_;
            $log = _$log_;

        });



    });


    it('should allow callbacks aggregation', function() {

        var pizzaDelivered = Restaurant.takeOrder('Margherita');

        // here's where the problem is
        // with current set up I can only call it as
        // pizzaDelivered.then(Person.eat, Person.beHungry);        
        pizzaDelivered.then(pawel.eat, pawel.beHungry);
        pizzaDelivered.then(pete.eat, pete.beHungry);

        Restaurant.deliveryOrder();

        expect($log.info.logs).toContain(['Pawel is eating delicious Margherita']);
        expect($log.info.logs).toContain(['Pete is eating delicious Margherita']);

    });


});

As I said - I'm new to it and would appreciate some help.

Spencer Mark
  • 5,263
  • 9
  • 29
  • 58

1 Answers1

1

The reason why the tests only allows

pizzaDelivered.then(Person.eat, Person.beHungry)

is because you have created a Person service. In Angular, services are singletons. The concept of a 'Person' does not entirely fit with the concept of singletons (ie. there can be more than 1 Person at any given time), but can be used within your application like so:

app = app.module('app', [])
  .controller('chicago', function($scope, $log) {

      $scope.family = [
         new Person('henry', $log),
         new Person('me', $log),
         new Person('you', $log)
       ];

   });

You should leave the Person and Restaurant as the book defined them. I believe that is the book's intention because within that definition there is unique line of code:

$rootScope.$digest();

http://docs.angularjs.org/guide/concepts This line invokes angular's digest cycle. It basically runs through all that is angular and updates the DOM and View with any changes that occurred within its context. For example, if your html had a binding to a $scope model:

<div ng-model='name'> {{ name }} </div>

and after after a few http calls to the server, this model changes within the angular part of the js, angular will automatically update this div to have the new name. However, when you have code that exists outside of the angular context, the $digest must be explicitly invoked as angular will not be aware within its context (its angular code), of values that have changed. The only time you might explicitly call $digest() is within directives, but most of the time it deals with 3rd party code's interaction with angular.

This would be my suggestion for moving forward:

Leave the Person and Restaurant code alone. Do not convert them into angular services. In a beforeEach function before the aggregating callback test, instantiate 2 Person objects

beforeEach(function() {    
    pete = new Person('pete', $log);
    pawel = new Person('pawel', $log);
});

One thing I noticed is that you said that the book's Person and Restaurant were global objects, but they are not. they have global functions/constructors.

this should make your tests pass.

Overall, I believe what the book is teaching you through this is to work with code that exists outside of angular. In many real-world applications, you barely work with angular js code by itself. Many times you have to work with third-party code such as GoogleMaps, Moment, etc.

something to keep in mind for the future: I do believe in the future angular versions (2.0), the project is heading in a direction to abstract the $digest cycle further and use an Observe() fn.

Alex
  • 36
  • 3
  • One thing however - you say that the objects defined above are not global - from my (limited) understanding, instantiation of the object in javascript outside of any other object - or in this case outside of Angular using variable assignment creates an object in the global namespace. Am I not right here? Could you please explain why you think that : var Person = function(name, $log) {} does not create a global object? – Spencer Mark Oct 13 '13 at 09:15
  • @SpencerMark you are right in that Person is in the global namespace. however, Person is a function and not an object. You're making me doubt myself as I am fairly new to JS :D – Alex Oct 13 '13 at 12:45
  • But isn't the function and object in JS? – Spencer Mark Oct 13 '13 at 16:27