AngularJS + Jasmine: Comparing objects

58,242

Solution 1

toEqual makes a deep equality comparison. Which means that when all the properties of the objects' values are equal, the objects are considered to be equal.

As you said, you are using resource which adds a couple of properties to the objects in the array.

So this {id:12} becomes this {id:12, $then: function, $resolved: true} which are not equal. Id checking should be fine if you are just testing if you set the values properly.

Solution 2

Short answer:

The existing answers all recommend either stringifying your objects, or creating a custom matcher/comparison function. But, there is an easier way: use angular.equals() in your Jasmine expect call, instead of using Jasmine's built-in toEqual matcher.

angular.equals() will ignore the additional properties added to your objects by Angular, whereas toEqual will fail the comparison for, say, $promise being on one of the objects.


Longer explanation:

I ran across this same problem in my AngularJS application. Let's set the scenario:

In my test, I created a local object and a local array, and expected them as responses to two GET requests. Afterwards, I compared the result of the GET's with the original object and array. I tested this using four different methods, and only one gave proper results.

Here's a portion of foobar-controller-spec.js:

var myFooObject = {id: 1, name: "Steve"};
var myBarsArray = [{id: 1, color: "blue"}, {id: 2, color: "green"}, {id: 3, color: "red"}];

...

beforeEach(function () {
    httpBackend.expectGET('/foos/1').respond(myFooObject);
    httpBackend.expectGET('/bars').respond(myBarsArray);
    
    httpBackend.flush();
});

it('should put foo on the scope', function () {
    expect(scope.foo).toEqual(myFooObject);
    //Fails with the error: "Expected { id : 1, name : 'Steve', $promise : { then : Function, catch : Function, finally : Function }, $resolved : true } to equal { id : 1, name : 'Steve' }."
    //Notice that the first object has extra properties...
    
    expect(scope.foo.toString()).toEqual(myFooObject.toString());
    //Passes, but invalid (see below)
    
    expect(JSON.stringify(scope.foo)).toEqual(JSON.stringify(myFooObject));
    //Fails with the error: "Expected '{"id":1,"name":"Steve","$promise":{},"$resolved":true}' to equal '{"id":1,"name":"Steve"}'."
    
    expect(angular.equals(scope.foo, myFooObject)).toBe(true);
    //Works as expected
});

it('should put bars on the scope', function () {
    expect(scope.bars).toEqual(myBarsArray);
    //Fails with the error: "Expected [ { id : 1, color : 'blue' }, { id : 2, color : 'green' }, { id : 3, color : 'red' } ] to equal [ { id : 1, color : 'blue' }, { id : 2, color : 'green' }, { id : 3, color : 'red' } ]."
    //Notice, however, that both arrays seem identical, which was the OP's problem as well.
    
    expect(scope.bars.toString()).toEqual(myBarsArray.toString());
    //Passes, but invalid (see below)
    
    expect(JSON.stringify(scope.bars)).toEqual(JSON.stringify(myBarsArray));
    //Works as expected
    
    expect(angular.equals(scope.bars, myBarsArray)).toBe(true);
    //Works as expected
});

For reference, here's the output from console.log using JSON.stringify() and .toString():

LOG: '***** myFooObject *****'
LOG: 'Stringified:{"id":1,"name":"Steve"}'
LOG: 'ToStringed:[object Object]'

LOG: '***** scope.foo *****'
LOG: 'Stringified:{"id":1,"name":"Steve","$promise":{},"$resolved":true}'
LOG: 'ToStringed:[object Object]'



LOG: '***** myBarsArray *****'
LOG: 'Stringified:[{"id":1,"color":"blue"},{"id":2,"color":"green"},{"id":3,"color":"red"}]'
LOG: 'ToStringed:[object Object],[object Object],[object Object]'

LOG: '***** scope.bars *****'
LOG: 'Stringified:[{"id":1,"color":"blue"},{"id":2,"color":"green"},{"id":3,"color":"red"}]'
LOG: 'ToStringed:[object Object],[object Object],[object Object]'

Notice how the stringified object has extra properties, and how toString yields invalid data which will give a false positive.

From looking at the above, here's a summary of the different methods:

  1. expect(scope.foobar).toEqual(foobar) : This fails both ways. When comparing objects, toString reveals that Angular has added extra properties. When comparing arrays, the contents seem identical, but this method still claims they are different.
  2. expect(scope.foo.toString()).toEqual(myFooObject.toString()) : This passes both ways. However, this is a false positive, since the objects are not being translated fully. The only assertion this makes is that the two arguments have the same number of objects.
  3. expect(JSON.stringify(scope.foo)).toEqual(JSON.stringify(myFooObject)) : This method gives the proper response when comparing arrays, but the object comparison has a similar fault to the raw comparison.
  4. expect(angular.equals(scope.foo, myFooObject)).toBe(true) : This is the correct way to make the assertion. By letting Angular do the comparison, it knows to ignore any properties which were added in the backend, and gives the proper result.

If it matters to anyone, I'm using AngularJS 1.2.14 and Karma 0.10.10, and testing on PhantomJS 1.9.7.

Solution 3

Long story short: add angular.equals as a jasmine matcher.

beforeEach(function(){
  this.addMatchers({
    toEqualData: function(expected) {
      return angular.equals(this.actual, expected);
    }
  });
});

So, then you can use it as follows:

it('should preselect first client in array', function() {
    //this passes:
    expect(scope.selected.client).toEqualData(RESPONSE[0]);

    //this fails:
    expect(scope.selected.client).toEqual(RESPONSE[0]);
});

Solution 4

I just had a similar problem and implemented a custom matcher as follows, based on many approaches:

beforeEach(function() {
  this.addMatchers({
    toBeSimilarTo: function(expected) {
      function buildObject(object) {
        var built = {};
        for (var name in object) {
          if (object.hasOwnProperty(name)) {
            built[name] = object[name];
          }
        }
        return built;
      }

      var actualObject = buildObject(this.actual);
      var expectedObject = buildObject(expected);
      var notText = this.isNot ? " not" : "";

      this.message = function () {
        return "Expected " + actualObject + notText + " to be similar to " + expectedObject;
      }

      return jasmine.getEnv().equals_(actualObject, expectedObject);

    }
  });
});

and then used this way:

it("gets the right data", function() {
  expect(scope.jobs[0]).toBeSimilarTo(myJob);
});

Of course, it's a very simple matcher and doesn't support many cases, but I didn't need anything more complex than that. You can wrap the matchers in a config file.

Check this answer for a similar implementation.

Solution 5

I had the same problem so I just called JSON.stringify() on the objects to be compared.

expect( JSON.stringify( $scope.angularResource ) == JSON.stringify( expectedValue )).toBe( true );
Share:
58,242

Related videos on Youtube

shaunlim
Author by

shaunlim

I'm curious about everything there is to know about building great products that people love.

Updated on August 23, 2022

Comments

  • shaunlim
    shaunlim over 1 year

    I'm just starting out writing tests for my AngularJS app and am doing so in Jasmine.

    Here are the relevant code snippets

    ClientController:

    'use strict';
    
    adminConsoleApp.controller('ClientController',
        function ClientController($scope, Client) {
    
            //Get list of clients
            $scope.clients = Client.query(function () {
                //preselect first client in array
                $scope.selected.client = $scope.clients[0];
            });
    
            //necessary for data-binding so that it is accessible in child scopes.
            $scope.selected = {};
    
            //Current page
            $scope.currentPage = 'start.html';
    
            //For Client nav bar
            $scope.clientNavItems = [
                {destination: 'features.html', title: 'Features'},
            ];
    
            //Set current page
            $scope.setCurrent = function (title, destination) {
                if (destination !== '') {
                    $scope.currentPage = destination;
                }
    
            };
    
            //Return path to current page
            $scope.getCurrent = function () {
                return 'partials/clients/' + $scope.currentPage;
            };
    
            //For nav bar highlighting of active page
            $scope.isActive = function (destination) {
                return $scope.currentPage === destination ? true : false;
            };
    
            //Reset current page on client change
            $scope.clientChange = function () {
                $scope.currentPage = 'start.html';
            };
        });
    

    ClientControllerSpec:

    'use strict';
    
    var RESPONSE = [
        {
            "id": 10,
            "name": "Client Plus",
            "ref": "client-plus"
        },
        {
            "id": 13,
            "name": "Client Minus",
            "ref": "client-minus"
        },
        {
            "id": 23805,
            "name": "Shaun QA",
            "ref": "saqa"
        }
    ];
    
    describe('ClientController', function() {
    
        var scope;
    
        beforeEach(inject(function($controller, $httpBackend, $rootScope) {
            scope = $rootScope;
            $httpBackend.whenGET('http://localhost:3001/clients').respond(RESPONSE);
            $controller('ClientController', {$scope: scope});
            $httpBackend.flush();
        }));
    
        it('should preselect first client in array', function() {
            //this fails.
            expect(scope.selected.client).toEqual(RESPONSE[0]);
        });
    
        it('should set current page to start.html', function() {
            expect(scope.currentPage).toEqual('start.html');
        });
    });
    

    The test fails:

    Chrome 25.0 (Mac) ClientController should preselect first client in array FAILED
        Expected { id : 10, name : 'Client Plus', ref : 'client-plus' } to equal { id : 10, name : 'Client Plus', ref : 'client-plus' }.
        Error: Expected { id : 10, name : 'Client Plus', ref : 'client-plus' } to equal { id : 10, name : 'Client Plus', ref : 'client-plus' }.
            at null.<anonymous> (/Users/shaun/sandbox/zong-admin-console-app/test/unit/controllers/ClientControllerSpec.js:43:39) 
    

    Does anyone have any ideas on why this might be happening?

    Also .. as I'm new to writing AngularJS tests, any comments on whether I'm setting up my test wrong or whether it can be improved will be welcome.

    Update:

    Including ClientService:

    'use strict';
    
    AdminConsoleApp.services.factory('Client', function ($resource) {
        //API is set up such that if clientId is passed in, will retrieve client by clientId, else retrieve all.
        return $resource('http://localhost:port/clients/:clientId', {port: ':3001', clientId: '@clientId'}, {
    
        });
    });
    

    Also, I got around the problem by comparing ids instead:

    it('should preselect first client in array', function () {
        expect(scope.selected.client.id).toEqual(RESPONSE[0].id);
    });
    
    • Ven
      Ven about 11 years
      It's possible jasmine is checking non-own properties. They won't show up in the JSON.stringify-ed version but they'll be taken into account for comparison
    • couzzi
      couzzi about 11 years
      Likely unrelated, but just to be safe, make sure you escape your port number; stranger things have happened... $httpBackend.whenGET('http://localhost\\:3001/clients').resp‌​ond(RESPONSE); I had a BLAST for an hour trying to decipher an error that ended up being an unescaped port number, FWIW.
    • shaunlim
      shaunlim about 11 years
      @couzzi thanks for the tip but when I tried escaping the port number like how you did, i get an exception: Error: Unexpected request: GET localhost:3001/clients No more request expected
    • shaunlim
      shaunlim about 11 years
      @user1737909 can you please elaborate a little? i got around the problem by comparing the 'id' field instead of the entire object but am still curious as to why the object comparison failed. one possibility is that scope.selected.client actually contains a $resource object..
    • Ven
      Ven about 11 years
      This explains it a bit more I think : stackoverflow.com/questions/8779249/…
  • kelv
    kelv almost 10 years
    This is great! Perhaps not what the asker was looking for, but it certainly helped me out - thanks!
  • The DIMM Reaper
    The DIMM Reaper over 9 years
    +1: Good idea building a matcher around angular.equals. My only concern would be that it could get confusing remembering which "equals" matcher you need in a given situation.
  • Lior Elrom
    Lior Elrom about 8 years
    JSON.stringify on an object in Jasmine might be equal to an empty object so the test itself will pass but it actually not checking the objects equality. See answers above.
  • Ceylan Mumun Kocabaş
    Ceylan Mumun Kocabaş almost 8 years
    this was very helpful, i was sure that my two objects are equal by content, but everything i tried said that they are not. angular.equals was my solution.
  • BillyTom
    BillyTom almost 8 years
    You can use JSON.stringify(someObject, null, 4) to prettify your objects before comparing them. If one of your assertions fails then the output will be much more readable.
  • hughes
    hughes over 7 years
    If using JSON.stringify, be aware that it does not guarantee that the object keys will be in the same order. {foo: 1, bar:2} will evaluate as not equal to {bar: 2, foo: 1}
  • The DIMM Reaper
    The DIMM Reaper over 7 years
    @hughes: Exactly right, that's just one reason why JSON.stringify can't be trusted to compare objects.
  • jannis
    jannis about 7 years
    Please note that JSON.stringify doesn't guarantee property order! So comparing generated JSONs might cause random test failures. It depends on the underlying javascript runner how properties will be sorted. For example: tests might work when run in engineA (e.g. NodeJS), but then break in engineB (e.g. Firefox) or even in another engineA version. Or break randomly even when run in the same engine of the same version.
  • hubatish
    hubatish almost 7 years
    IMO, toAngularEqual better expresses why these two are different. What text does this print if they are not equal? Not a big fan of just getting the error message 'Expected false to be equal to true'.
  • The DIMM Reaper
    The DIMM Reaper over 6 years
    @hubatish I believe you're referencing jasmine-moar-matchers. Simple, yet effective.