Question

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 
  } 
} ].
Was it helpful?

Solution

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);
};

OTHER TIPS

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
    },

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.

Use

JSON.stringify(obj, undefined, 2)

The 3th parameter is the indentation level

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),
    )
  })
})
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top