Make toEqual matcher report the difference between objects

- Mismatches deep within object/array structures are pinpointed with a JsonPath-like syntax.
This commit is contained in:
Ben Christel
2016-07-18 15:17:00 -07:00
committed by Ben Christel
parent 5a76e59d5b
commit d5e6bf47ed
17 changed files with 2251 additions and 1156 deletions

View File

@@ -99,6 +99,11 @@ describe("jasmineUnderTest.pp", function () {
}, bar: [1, 2, 3]})).toEqual("Object({ foo: Function, bar: [ 1, 2, 3 ] })");
});
it("should print 'null' as the constructor of an object with its own constructor property", function() {
expect(jasmineUnderTest.pp({constructor: function() {}})).toEqual("null({ constructor: Function })");
expect(jasmineUnderTest.pp({constructor: 'foo'})).toEqual("null({ constructor: 'foo' })");
});
it("should not include inherited properties when stringifying an object", function() {
var SomeClass = function SomeClass() {};
SomeClass.prototype.foo = "inherited foo";
@@ -164,7 +169,6 @@ describe("jasmineUnderTest.pp", function () {
}
});
it('should not do HTML escaping of strings', function() {
expect(jasmineUnderTest.pp('some <b>html string</b> &', false)).toEqual('\'some <b>html string</b> &\'');
});

View File

@@ -63,4 +63,37 @@ describe("jasmineUnderTest.util", function() {
expect(actual).toEqual(expected);
});
});
describe("objectDifference", function() {
it("given two objects A and B, returns the properties in A not present in B", function() {
var a = {
foo: 3,
bar: 4,
baz: 5
};
var b = {
bar: 6,
quux: 7
};
expect(jasmineUnderTest.util.objectDifference(a, b)).toEqual({foo: 3, baz: 5})
});
it("only looks at own properties of both objects", function() {
function Foo() {}
Foo.prototype.x = 1;
Foo.prototype.y = 2;
var a = new Foo();
a.x = 1;
var b = new Foo();
b.y = 2;
expect(jasmineUnderTest.util.objectDifference(a, b)).toEqual({x: 1});
expect(jasmineUnderTest.util.objectDifference(b, a)).toEqual({y: 2});
})
})
});

View File

@@ -79,6 +79,30 @@ describe("Custom Matchers (Integration)", function() {
env.execute();
});
it("displays an appropriate failure message if a custom equality matcher fails", function(done) {
env.it("spec using custom equality matcher", function() {
var customEqualityFn = function(a, b) {
// "foo" is not equal to anything
if (a === 'foo' || b === 'foo') {
return false;
}
};
env.addCustomEqualityTester(customEqualityFn);
env.expect({foo: 'foo'}).toEqual({foo: 'foo'});
});
var specExpectations = function(result) {
expect(result.status).toEqual('failed');
expect(result.failedExpectations[0].message).toEqual(
"Expected $.foo = 'foo' to equal 'foo'."
);
};
env.addReporter({ specDone: specExpectations, jasmineDone: done });
env.execute();
});
it("uses the negative compare function for a negative comparison, if provided", function(done) {
env.it("spec with custom negative comparison matcher", function() {
env.addMatchers({

View File

@@ -0,0 +1,47 @@
describe("DiffBuilder", function() {
it("records the actual and expected objects", function() {
var diffBuilder = jasmineUnderTest.DiffBuilder();
diffBuilder.record({x: 'actual'}, {x: 'expected'});
expect(diffBuilder.getMessage()).toEqual("Expected Object({ x: 'actual' }) to equal Object({ x: 'expected' }).");
});
it("prints the path at which the difference was found", function() {
var diffBuilder = jasmineUnderTest.DiffBuilder();
diffBuilder.withPath('foo', function() {
diffBuilder.record({x: 'actual'}, {x: 'expected'});
});
expect(diffBuilder.getMessage()).toEqual("Expected $.foo = Object({ x: 'actual' }) to equal Object({ x: 'expected' }).");
});
it("prints multiple messages, separated by newlines", function() {
var diffBuilder = jasmineUnderTest.DiffBuilder();
diffBuilder.withPath('foo', function() {
diffBuilder.record(1, 2);
});
var message =
"Expected $.foo = 1 to equal 2.\n" +
"Expected 3 to equal 4.";
diffBuilder.record(3, 4);
expect(diffBuilder.getMessage()).toEqual(message);
});
it("allows customization of the message", function() {
var diffBuilder = jasmineUnderTest.DiffBuilder();
function darthVaderFormatter(actual, expected, path) {
return "I find your lack of " + expected + " disturbing. (was " + actual + ", at " + path + ")"
}
diffBuilder.withPath('x', function() {
diffBuilder.record('bar', 'foo', darthVaderFormatter);
});
expect(diffBuilder.getMessage()).toEqual("I find your lack of foo disturbing. (was bar, at $.x)");
});
});

View File

@@ -0,0 +1,13 @@
describe('NullDiffBuilder', function() {
it('responds to withPath() by calling the passed function', function() {
var spy = jasmine.createSpy('callback');
jasmineUnderTest.NullDiffBuilder().withPath('does not matter', spy);
expect(spy).toHaveBeenCalled();
});
it('responds to record()', function() {
expect(function() {
jasmineUnderTest.NullDiffBuilder().record('does not matter');
}).not.toThrow();
})
});

View File

@@ -0,0 +1,43 @@
describe('ObjectPath', function() {
var ObjectPath = jasmineUnderTest.ObjectPath;
it('represents the path to a node in an object tree', function() {
expect(new ObjectPath(['foo', 'bar']).toString()).toEqual('$.foo.bar');
});
it('has a depth', function() {
expect(new ObjectPath().depth()).toEqual(0);
expect(new ObjectPath(['foo']).depth()).toEqual(1);
});
it('renders numbers as array access', function() {
expect(new ObjectPath(['foo', 0]).toString()).toEqual('$.foo[0]');
});
it('renders properties that are valid identifiers with dot notation', function() {
expect(new ObjectPath(['foo123']).toString()).toEqual('$.foo123');
expect(new ObjectPath(['x_y']).toString()).toEqual('$.x_y');
expect(new ObjectPath(['A$B']).toString()).toEqual('$.A$B');
});
it('renders properties with non-identifier-safe characters with square bracket notation', function() {
expect(new ObjectPath(['a b c']).toString()).toEqual("$['a b c']");
expect(new ObjectPath(['1hello']).toString()).toEqual("$['1hello']");
});
it('renders as the empty string when empty', function() {
expect(new ObjectPath().toString()).toEqual('');
});
it('stringifies properties that are not strings or numbers', function() {
expect(new ObjectPath([{}]).toString()).toEqual("$['[object Object]']");
});
it('can be created based on another path', function() {
var root = new ObjectPath();
var path = root.add('foo');
expect(path.toString()).toEqual('$.foo');
expect(root.toString()).toEqual('');
})
});

View File

@@ -61,10 +61,14 @@ describe("matchersUtil", function() {
expect(jasmineUnderTest.matchersUtil.equals(foo, [undefined])).toBe(true);
});
it("fails for Arrays that are not equivalent", function() {
it("fails for Arrays that have different lengths", function() {
expect(jasmineUnderTest.matchersUtil.equals([1, 2], [1, 2, 3])).toBe(false);
});
it("fails for Arrays that have different elements", function() {
expect(jasmineUnderTest.matchersUtil.equals([1, 2, 3], [1, 5, 3])).toBe(false);
});
it("fails for Arrays whose contents are equivalent, but have differing properties", function() {
var one = [1,2,3],
two = [1,2,3];
@@ -136,6 +140,13 @@ describe("matchersUtil", function() {
expect(jasmineUnderTest.matchersUtil.equals(actual, expected)).toBe(false);
});
it("fails for Objects that have the same number of keys, but different keys/values", function () {
var expected = { a: undefined },
actual = { b: 1 };
expect(jasmineUnderTest.matchersUtil.equals(actual, expected)).toBe(false);
})
it("fails when comparing an empty object to an empty array (issue #114)", function() {
var emptyObject = {},
emptyArray = [];

View File

@@ -1,20 +1,39 @@
describe("toEqual", function() {
"use strict";
function compareEquals(actual, expected) {
var util = jasmineUnderTest.matchersUtil,
matcher = jasmineUnderTest.matchers.toEqual(util);
var result = matcher.compare(actual, expected);
return result;
}
it("delegates to equals function", function() {
var util = {
equals: jasmine.createSpy('delegated-equals').and.returnValue(true)
equals: jasmine.createSpy('delegated-equals').and.returnValue(true),
buildFailureMessage: function() {
return 'does not matter'
},
DiffBuilder: jasmineUnderTest.matchersUtil.DiffBuilder
},
matcher = jasmineUnderTest.matchers.toEqual(util),
result;
result = matcher.compare(1, 1);
expect(util.equals).toHaveBeenCalledWith(1, 1, []);
expect(util.equals).toHaveBeenCalledWith(1, 1, [], jasmine.anything());
expect(result.pass).toBe(true);
});
it("delegates custom equality testers, if present", function() {
var util = {
equals: jasmine.createSpy('delegated-equals').and.returnValue(true)
equals: jasmine.createSpy('delegated-equals').and.returnValue(true),
buildFailureMessage: function() {
return 'does not matter'
},
DiffBuilder: jasmineUnderTest.matchersUtil.DiffBuilder
},
customEqualityTesters = ['a', 'b'],
matcher = jasmineUnderTest.matchers.toEqual(util, customEqualityTesters),
@@ -22,7 +41,457 @@ describe("toEqual", function() {
result = matcher.compare(1, 1);
expect(util.equals).toHaveBeenCalledWith(1, 1, ['a', 'b']);
expect(util.equals).toHaveBeenCalledWith(1, 1, ['a', 'b'], jasmine.anything());
expect(result.pass).toBe(true);
});
it("reports the difference between objects that are not equal", function() {
var actual = {x: 1, y: 3},
expected = {x: 2, y: 3},
message = "Expected $.x = 1 to equal 2.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports the difference between nested objects that are not equal", function() {
var actual = {x: {y: 1}},
expected = {x: {y: 2}},
message = "Expected $.x.y = 1 to equal 2.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("formats property access so that it's valid JavaScript", function() {
var actual = {'my prop': 1},
expected = {'my prop': 2},
message = "Expected $['my prop'] = 1 to equal 2.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports missing properties", function() {
var actual = {x: {}},
expected = {x: {y: 1}},
message =
"Expected $.x to have properties\n" +
" y: 1";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports extra properties", function() {
var actual = {x: {y: 1, z: 2}},
expected = {x: {}},
message =
"Expected $.x not to have properties\n" +
" y: 1\n" +
" z: 2";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("pretty-prints properties", function() {
var actual = {x: {y: 'foo bar'}},
expected = {x: {}},
message =
"Expected $.x not to have properties\n" +
" y: 'foo bar'"
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports extra and missing properties together", function() {
var actual = {x: {y: 1, z: 2, f: 4}},
expected = {x: {y: 1, z: 2, g: 3}},
message =
"Expected $.x to have properties\n" +
" g: 3\n" +
"Expected $.x not to have properties\n" +
" f: 4";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports extra and missing properties of the root-level object", function() {
var actual = {x: 1},
expected = {a: 1},
message =
"Expected object to have properties\n" +
" a: 1\n" +
"Expected object not to have properties\n" +
" x: 1";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports multiple incorrect values", function() {
var actual = {x: 1, y: 2},
expected = {x: 3, y: 4},
message =
"Expected $.x = 1 to equal 3.\n" +
"Expected $.y = 2 to equal 4.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatch between actual child object and expected child number", function() {
var actual = {x: {y: 2}},
expected = {x: 1},
message = "Expected $.x = Object({ y: 2 }) to equal 1.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("uses the default failure message if actual is not an object", function() {
var actual = 1,
expected = {x: {}},
message = "Expected 1 to equal Object({ x: Object({ }) }).";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("uses the default failure message if expected is not an object", function() {
var actual = {x: {}},
expected = 1,
message = "Expected Object({ x: Object({ }) }) to equal 1.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("uses the default failure message given arrays with different lengths", function() {
var actual = [1, 2],
expected = [1, 2, 3],
message =
"Expected [ 1, 2 ] to equal [ 1, 2, 3 ].";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports a mismatch between elements of equal-length arrays", function() {
var actual = [1, 2, 5],
expected = [1, 2, 3],
message = "Expected $[2] = 5 to equal 3.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports a mismatch between multiple array elements", function() {
var actual = [2, 2, 5],
expected = [1, 2, 3],
message =
"Expected $[0] = 2 to equal 1.\n" +
"Expected $[2] = 5 to equal 3.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports a mismatch between properties of objects in arrays", function() {
var actual = [{x: 1}],
expected = [{x: 2}],
message = "Expected $[0].x = 1 to equal 2.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports a mismatch between arrays in objects", function() {
var actual = {x: [1]},
expected = {x: [2]},
message =
"Expected $.x[0] = 1 to equal 2.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches between nested arrays", function() {
var actual = [[1]],
expected = [[2]],
message =
"Expected $[0][0] = 1 to equal 2.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches involving NaN", function() {
var actual = {x: 0},
expected = {x: 0/0},
message = "Expected $.x = 0 to equal NaN.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches involving regular expressions", function() {
var actual = {x: '1'},
expected = {x: /1/},
message = "Expected $.x = '1' to equal /1/.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches involving infinities", function() {
var actual = {x: 0},
expected = {x: 1/0},
message = "Expected $.x = 0 to equal Infinity.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches involving booleans", function() {
var actual = {x: false},
expected = {x: true},
message = "Expected $.x = false to equal true.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches involving strings", function() {
var actual = {x: 'foo'},
expected = {x: 'bar'},
message = "Expected $.x = 'foo' to equal 'bar'.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches involving undefined", function() {
var actual = {x: void 0},
expected = {x: 0},
message = "Expected $.x = undefined to equal 0.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches involving null", function() {
var actual = {x: null},
expected = {x: 0},
message = "Expected $.x = null to equal 0.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches between objects with different constructors", function () {
function Foo() {}
function Bar() {}
var actual = {x: new Foo()},
expected = {x: new Bar()},
message = "Expected $.x to be a kind of Bar, but was Foo({ }).";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports type mismatches at the root level", function () {
function Foo() {}
function Bar() {}
var actual = new Foo(),
expected = new Bar(),
message = "Expected object to be a kind of Bar, but was Foo({ }).";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches between objects with their own constructor property", function () {
function Foo() {}
function Bar() {}
var actual = {x: {constructor: 'blerf'}},
expected = {x: {constructor: 'ftarrh'}},
message = "Expected $.x.constructor = 'blerf' to equal 'ftarrh'.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches between an object with a real constructor and one with its own constructor property", function () {
function Foo() {}
function Bar() {}
var actual = {x: {}},
expected = {x: {constructor: 'ftarrh'}},
message =
"Expected $.x to have properties\n" +
" constructor: 'ftarrh'";
expect(compareEquals(actual, expected).message).toEqual(message);
expect(compareEquals(expected, actual).message).toEqual(
"Expected $.x not to have properties\n constructor: 'ftarrh'"
);
});
it("reports mismatches between 0 and -0", function() {
var actual = {x: 0},
expected = {x: -0},
message = "Expected $.x = 0 to equal -0.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches between Errors", function() {
var actual = {x: new Error("the error you got")},
expected = {x: new Error("the error you want")},
message = "Expected $.x = Error: the error you got to equal Error: the error you want.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches between Functions", function() {
var actual = {x: function() {}},
expected = {x: function() {}},
message = "Expected $.x = Function to equal Function.";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("does not report deep mismatches within Sets", function() {
// TODO: implement deep comparison of set elements
jasmine.getEnv().requireFunctioningSets();
var actual = new Set([1]),
expected = new Set([2]),
message = 'Expected Set( 1 ) to equal Set( 2 ).';
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches between Sets nested in objects", function() {
jasmine.getEnv().requireFunctioningSets();
var actual = {sets: [new Set([1])]},
expected = {sets: [new Set([2])]},
message = "Expected $.sets[0] = Set( 1 ) to equal Set( 2 ).";
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports mismatches between Sets of different lengths", function() {
jasmine.getEnv().requireFunctioningSets();
var actual = new Set([1, 2]),
expected = new Set([2]),
message = 'Expected Set( 1, 2 ) to equal Set( 2 ).';
expect(compareEquals(actual, expected).message).toEqual(message);
});
function isNotRunningInBrowser() {
return typeof document === 'undefined'
}
it("reports mismatches between DOM nodes with different tags", function() {
if(isNotRunningInBrowser()) {
return;
}
var actual = {a: document.createElement('div')},
expected = {a: document.createElement('p')},
message = 'Expected $.a = HTMLNode to equal HTMLNode.';
expect(compareEquals(actual, expected).message).toEqual(message);
});
it('reports mismatches between DOM nodes with different content', function() {
if(isNotRunningInBrowser()) {
return;
}
var nodeA = document.createElement('div'),
nodeB = document.createElement('div');
nodeA.innerText = 'foo'
nodeB.innerText = 'bar'
var actual = {a: nodeA},
expected = {a: nodeB},
message = 'Expected $.a = HTMLNode to equal HTMLNode.';
expect(compareEquals(actual, expected).message).toEqual(message);
})
it("reports mismatches between a DOM node and a bare Object", function() {
if(isNotRunningInBrowser()) {
return;
}
var actual = {a: document.createElement('div')},
expected = {a: {}},
message = 'Expected $.a = HTMLNode to equal Object({ }).';
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports asymmetric mismatches", function() {
var actual = {a: 1},
expected = {a: jasmineUnderTest.any(String)},
message = 'Expected $.a = 1 to equal <jasmine.any(String)>.';
expect(compareEquals(actual, expected).message).toEqual(message);
expect(compareEquals(actual, expected).pass).toBe(false)
});
it("reports asymmetric mismatches when the asymmetric comparand is the actual value", function() {
var actual = {a: jasmineUnderTest.any(String)},
expected = {a: 1},
message = 'Expected $.a = <jasmine.any(String)> to equal 1.';
expect(compareEquals(actual, expected).message).toEqual(message);
expect(compareEquals(actual, expected).pass).toBe(false)
});
it("does not report a mismatch when asymmetric matchers are satisfied", function() {
var actual = {a: 'a'},
expected = {a: jasmineUnderTest.any(String)},
message = 'Expected $.a = 1 to equal <jasmine.any(String)>.';
expect(compareEquals(actual, expected).pass).toBe(true)
});
it("works on big complex stuff", function() {
var actual = {
foo: [
{bar: 1, things: ['a', 'b']},
{bar: 2, things: ['a', 'b']}
],
baz: [
{a: {b: 1}}
],
quux: 1,
nan: 0,
aRegexp: 'hi',
inf: -1/0,
boolean: false,
notDefined: 0,
aNull: void 0
}
var expected = {
foo: [
{bar: 2, things: ['a', 'b', 'c']},
{bar: 2, things: ['a', 'd']}
],
baz: [
{a: {b: 1, c: 1}}
],
quux: [],
nan: 0/0,
aRegexp: /hi/,
inf: 1/0,
boolean: true,
notDefined: void 0,
aNull: null
}
var message =
'Expected $.foo[0].bar = 1 to equal 2.\n' +
"Expected $.foo[0].things = [ 'a', 'b' ] to equal [ 'a', 'b', 'c' ].\n" +
"Expected $.foo[1].things[1] = 'b' to equal 'd'.\n" +
'Expected $.baz[0].a to have properties\n' +
' c: 1\n' +
'Expected $.quux = 1 to equal [ ].\n' +
'Expected $.nan = 0 to equal NaN.\n' +
"Expected $.aRegexp = 'hi' to equal /hi/.\n" +
'Expected $.inf = -Infinity to equal Infinity.\n' +
'Expected $.boolean = false to equal true.\n' +
'Expected $.notDefined = 0 to equal undefined.\n' +
'Expected $.aNull = undefined to equal null.'
expect(compareEquals(actual, expected).message).toEqual(message);
})
});