10

I'm currently using the Karma test runner for my Angular project, with the jasmine testing framework. It's working great, but I have one issue: When an object comparison fails, the resulting print into the console is really hard to read, and gets harder the more properties these objects have. Example:

Expected spy spy to have been called with [ { currentCareMoment : { ID : 5, Description : 'Late namiddag (16-20)', StartHour : 16, EndHour : 20 }, previousCareMoment : { ID : 4, Description : 'Namiddag (14-16)', StartHour : 14, EndHour : 16 } } ] but actual calls were [ { currentCareMoment : { ID : 6, Description : 'Avond (20-24)', StartHour : 20, EndHour : 24 }, previousCareMoment : { ID : 5, Description : 'Late namiddag (16-20)', StartHour : 16, EndHour : 20 } } ].

Is there anyway to set up Jasmine (as I think Karma has nothing to do with it) to print objects prettier? Just some line breaks and indentation would already be a huge help. Example:

Expected spy spy to have been called with [ { 
  currentCareMoment : { 
    ID : 5, 
    Description : 'Late namiddag (16-20)', 
    StartHour : 16, 
    EndHour : 20 
  }, 
  previousCareMoment : { 
    ID : 4, 
    Description : 'Namiddag (14-16)', 
    StartHour : 14, 
    EndHour : 16 
  } 
} ] but actual calls were [ { 
  currentCareMoment : { 
    ID : 6, 
    Description : 'Avond (20-24)', 
    StartHour : 20, 
    EndHour : 24 
  }, 
  previousCareMoment : { 
    ID : 5, 
    Description : 'Late namiddag (16-20)', 
    StartHour : 16, 
    EndHour : 20 
  } 
} ].
  • Possible duplicate http://stackoverflow.com/questions/23429709/karma-jasmine-tests-highlight-diff-in-terminal But unfortunately, no solution so far – 23tux May 28 '14 at 13:16
  • try use node.js : [util.inspect](https://nodejs.org/api/util.html#util_util_inspect_object_options) – boldnik Jan 14 '16 at 16:08

5 Answers5

15

My answer is based on jasmine 2.0.1.

Method 1 is documented in the jasmine docs. So it is probably recommended.
Method 2 however is much simpler.

Method 1: Using a custom matcher

My initial though was to create a custom matcher as described here. So I copied the toHaveBeenCalledWith matcher from the jasmine source code and modified it so it would pretty print:

var matchers = {
  toBeCalledWith: function (util, customEqualityTesters) {
    return {
      compare: function() {
        var args = Array.prototype.slice.call(arguments, 0),
          actual = args[0],
          expectedArgs = args.slice(1),
          result = { pass: false };

        if (!jasmine.isSpy(actual)) {
          throw new Error('Expected a spy, but got ' + jasmine.JSON.stringify(actual, undefined, 2) + '.');
        }

        if (!actual.calls.any()) {
          result.message = function() {
            return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + JSON.stringify(expectedArgs, undefined, 2) + ' but it was never called.';
          };
          return result;
        }

        if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) {
          result.pass = true;
          result.message = function() {
            return 'Expected spy ' + actual.and.identity() + ' not to have been called with ' + JSON.stringify(expectedArgs, undefined, 2) + ' but it was.';
          };
        } else {
          result.message = function() {
            return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + JSON.stringify(expectedArgs, undefined, 2) + ' but actual calls were ' + JSON.stringify(actual.calls.allArgs(), undefined, 2) + '.';
          };
        }

        return result;
      }
    };
  }
};

The test case would then use our new matcher instead:

describe('Test', function() {

  beforeEach(function() {
    jasmine.addMatchers(matchers);
  });

  it('should print pretty', function() {
    var spy = jasmine.createSpy('spy');
    spy({
      currentCareMoment: {
      ID: 5,
      Description: 'Late namiddag (16-20)',
      StartHour: 16,
      EndHour: 20
    },
    previousCareMoment: {
      ID: 4,
      Description: 'Namiddag (14-16)',
      StartHour: 14,
      EndHour: 16
    }});

    expect(spy).toBeCalledWith({
      currentCareMoment: {
        ID: 6,
        Description: 'Avond (20-24)',
        StartHour: 20,
        EndHour: 24
      },
      previousCareMoment: {
        ID: 5,
        Description: 'Late namiddag (16-20)',
        StartHour: 16,
        EndHour: 20
      }
    });
  });
});

Method 2: Override jasmine.pp

However, during implementing this I noticed jasmine uses the function jasmine.pp for pretty printing. So I figured I could just override this by adding the following on top of my test file:

jasmine.pp = function(obj) {
  return JSON.stringify(obj, undefined, 2);
};
Remco Haszing
  • 7,178
  • 4
  • 40
  • 83
  • 1
    Very nice, Remco. I ended up putting this in a separate settings.js file in my test folder. I then include that at the top of the files array in karma.conf.js so it's available to everything. – Eric Fuller Nov 18 '15 at 06:36
8

Since the time that the other answers here were added, a pretty-printing option became available in karma-jasmine-diff-reporter. I would suggest trying it -- it's very configurable and is working nicely for me in combination with other common test reporters.

A minimal configuration looks like:

    reporters: ['jasmine-diff'],

    jasmineDiffReporter: {
        multiline: true,
        pretty: true
    },
Ben Regenspan
  • 10,058
  • 2
  • 33
  • 44
  • 1
    I suppose the 'dots' should not be present in the array, if 'jasmine-diff' is. Your current example outputs the report twice. – d.k Nov 28 '16 at 09:50
5

I found that overriding jasmine.pp caused my spec reporters to no longer color-code actual vs. expected diffs.

My solution was to add the below snippet to it's own file, load it into karma.conf, then add the custom matcher (using underscore for asserting deep equality) to the config of the reporter that produces color-coded diffs in the console (karma-jasmine-diff-reporter)

//This will run before all of our specs because it's outside of a describe block
beforeEach(function() {
  var objectMatcher = {
    toEqualObject: function(util, customEqualityTesters) {
      return {
        compare: function(actual, expected) {
          var result = {};
          result.pass = _.isEqual(actual, expected);
          if (result.pass) {
            result.message = "Expected \n" + JSON.stringify(actual, null, 2) + "\n not to equal \n" + JSON.stringify(expected, null, 2) + "\n";
          } else {
            result.message = "Expected \n" + JSON.stringify(actual, null, 2) + "\n to equal \n" + JSON.stringify(expected, null, 2) + "";
          }
          return result;
        }
      };
    }
  };
  jasmine.addMatchers(objectMatcher);
});

Now I can get output like this in the console, by calling expect(foo).toEqualObject(bar):

Pretty console output

Figuring out how to make this work with jasmine spies is left as an exercise for the reader.

0

Use

JSON.stringify(obj, undefined, 2)

The 3th parameter is the indentation level

Giovanni Bitliner
  • 2,032
  • 5
  • 31
  • 50
0

Here is a jasmine custom matcher that removes call arguments from the results that match an Any term in the 'expected'. Node's util.inspect is used to handle circular references and pretty print results.

Output:

  custom matcher
    1) should not display arguments corresponding to expected Any arguments

  0 passing (15ms)
  1 failing

  1) custom matcher
       should not display arguments corresponding to expected Any arguments:
     ExpectationFailed: Expected spy underTest to have been called with [
  Any { expectedObject: [Function: Object] },
  { name: 'Bob' },
  { name: 'New York' },
  Any { expectedObject: [Function: Object] }
] but actual calls were [
  [
    '<Hidden due to Any match>',
    { name: 'Joe' },
    { name: 'New York' },
    '<Hidden due to Any match>'
  ]
].

toHaveBeenCalledWith.js

'use strict'

import { inspect } from 'util'

export function toHaveBeenCalledWith2 (util, customEqualityTesters) {
  return {
    compare: function () {
      const args = Array.prototype.slice.call(arguments, 0)
      const actual = args[0]
      const expectedArgs = args.slice(1)
      const result = { pass: false }

      if (!isSpyLike(actual)) {
        throw new Error('Expected a spy, but got ' + pp(actual) + '.')
      }

      if (!actual.calls.any()) {
        result.message = function () { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + pp(expectedArgs) + ' but it was never called.'; }
        return result
      }

      if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) {
        result.pass = true
        result.message = function () { return 'Expected spy ' + actual.and.identity() + ' not to have been called with ' + pp(expectedArgs) + ' but it was.' }
      } else {
        const anyIndexes = getIndexesOfJasmineAnyArgs(expectedArgs)
        const actualArgs = stripJasmineAnyArgsFromActual(actual.calls.allArgs(), anyIndexes)
        result.message = function () { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + pp(expectedArgs) + ' but actual calls were ' + pp(actualArgs) + '.' }
      }

      return result
    },
  }
}

function stripJasmineAnyArgsFromActual (actualArgsList, indexesToIgnore) {
  const strippedArgs = []
  actualArgsList.forEach(args => {
    const stripped = args.map((arg, argIndex) => {
      if (indexesToIgnore.includes(argIndex)) {
        return '<Hidden due to Any match>'
      } else {
        return arg
      }
    })
    strippedArgs.push(stripped)
  })
  return strippedArgs
}

function getIndexesOfJasmineAnyArgs (expectedArgs) {
  const anyIndexes = []
  expectedArgs.forEach((arg, i) => {
    if (arg.constructor.name === 'Any') {
      anyIndexes.push(i)
    }
  })
  return anyIndexes
}

function isSpyLike (possibleSpy) {
  if (!possibleSpy) {
    return false
  }
  return possibleSpy.and && possibleSpy.and.constructor.name === 'SpyStrategy' &&
    possibleSpy.calls && possibleSpy.calls.constructor.name === 'CallTracker'
}

function pp (args) {
  return inspect(args, { depth: 5 })
}

Usage:

import { toHaveBeenCalledWith2 } from '../matchers/toHaveBeenCalledWith'

describe('custom matcher', function () {

  beforeEach(async function () {
    const matchers = {
      toHaveBeenCalledWith2,
    }
    jasmine.addMatchers(matchers)
  })

  it('should not display arguments corresponding to expected Any arguments', function () {
    const obj = {
      underTest: function (a, person, city, d) { },
    }
    const expectedPerson = {
      name: 'Bob',
    }
    const expectedCity = {
      name: 'New York',
    }

    spyOn(obj, 'underTest')

    obj.underTest({}, { name: 'Joe' }, { name: 'New York' }, {})

    expect(obj.underTest).toHaveBeenCalledWith2(
      jasmine.any(Object),
      expectedPerson,
      expectedCity,
      jasmine.any(Object),
    )
  })
})
alanning
  • 5,198
  • 2
  • 34
  • 33