Repository: chaijs/chai
Pull request #760 (2016-07-28 00:09:54)

Breaking: Fix `.include` to always use strict equality
- Previously, `.include` was using strict equality for non-negated property
inclusion, but deep equality for negated property inclusion and array
inclusion. This fix causes `.include` to always use strict equality.

Note:

- #744, #757, and #758 are prerequisites for this PR. I recommend reviewing them first, and then only reviewing the newest commit in this PR.
- This PR is a prerequisite for completing #743.

keithamus: Yup, LGTM. @lucasfcosta?

Patch:
diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js
index f7b5dba..531220c 100644
--- a/lib/chai/core/assertions.js
+++ b/lib/chai/core/assertions.js
@@ -7,6 +7,7 @@
 
 module.exports = function (chai, _) {
   var Assertion = chai.Assertion
+    , AssertionError = chai.AssertionError
     , toString = Object.prototype.toString
     , flag = _.flag;
 
@@ -70,9 +71,13 @@ module.exports = function (chai, _) {
   /**
    * ### .deep
    *
-   * Sets the `deep` flag, later used by the `equal` assertion.
+   * Sets the `deep` flag, later used by the `equal`, `members`, and `property`
+   * assertions.
    *
-   *     expect(foo).to.deep.equal({ bar: 'baz' });
+   *     const obj = {a: 1};
+   *     expect(obj).to.deep.equal({a: 1});
+   *     expect([obj]).to.have.deep.members([{a: 1}]);
+   *     expect({foo: obj}).to.have.deep.property('foo', {a: 1});
    *
    * @name deep
    * @namespace BDD
@@ -217,6 +222,22 @@ module.exports = function (chai, _) {
    *     expect([1,2,3]).to.include(2);
    *     expect('foobar').to.contain('foo');
    *     expect({ foo: 'bar', hello: 'universe' }).to.include({ foo: 'bar' });
+   * 
+   * By default, strict equality (===) is used. When asserting the inclusion of
+   * a value in an array, the array is searched for an element that's strictly
+   * equal to the given value. When asserting a subset of properties in an
+   * object, the object is searched for the given property keys, checking that
+   * each one is present and stricty equal to the given property value. For
+   * instance:
+   *
+   *     var obj1 = {a: 1}
+   *       , obj2 = {b: 2};
+   *     expect([obj1, obj2]).to.include(obj1);
+   *     expect([obj1, obj2]).to.not.include({a: 1});
+   *     expect({foo: obj1, bar: obj2}).to.include({foo: obj1});
+   *     expect({foo: obj1, bar: obj2}).to.include({foo: obj1, bar: obj2});
+   *     expect({foo: obj1, bar: obj2}).to.not.include({foo: {a: 1}});
+   *     expect({foo: obj1, bar: obj2}).to.not.include({foo: obj1, bar: {b: 2}});
    *
    * These assertions can also be used as property based language chains,
    * enabling the `contains` flag for the `keys` assertion. For instance:
@@ -242,28 +263,44 @@ module.exports = function (chai, _) {
 
     if (msg) flag(this, 'message', msg);
     var obj = flag(this, 'object');
-    var expected = false;
 
-    if (_.type(obj) === 'array' && _.type(val) === 'object') {
-      for (var i in obj) {
-        if (_.eql(obj[i], val)) {
-          expected = true;
-          break;
+    // This block is for asserting a subset of properties in an object.
+    if (_.type(obj) === 'object') {
+      var props = Object.keys(val)
+        , negate = flag(this, 'negate')
+        , firstErr = null
+        , numErrs = 0;
+
+      props.forEach(function (prop) {
+        var propAssertion = new Assertion(obj);
+        _.transferFlags(this, propAssertion, false);
+
+        if (!negate || props.length === 1) {
+          propAssertion.property(prop, val[prop]);
+          return;
         }
-      }
-    } else if (_.type(val) === 'object') {
-      if (!flag(this, 'negate')) {
-        for (var k in val) new Assertion(obj).property(k, val[k]);
-        return;
-      }
-      var subset = {};
-      for (var k in val) subset[k] = obj[k];
-      expected = _.eql(subset, val);
-    } else {
-      expected = (obj != undefined) && ~obj.indexOf(val);
+
+        try {
+          propAssertion.property(prop, val[prop]);
+        } catch (err) {
+          if (!_.checkError.compatibleConstructor(err, AssertionError)) throw err;
+          if (firstErr === null) firstErr = err;
+          numErrs++;
+        }
+      }, this);
+
+      // When validating .not.include with multiple properties, we only want
+      // to throw an assertion error if all of the properties are included,
+      // in which case we throw the first property assertion error that we
+      // encountered.
+      if (negate && props.length > 1 && numErrs === props.length) throw firstErr;
+
+      return;
     }
+    
+    // Assert inclusion in an array or substring in a string.
     this.assert(
-        expected
+        typeof obj !== "undefined" && typeof obj !== "null" && ~obj.indexOf(val)
       , 'expected #{this} to include ' + _.inspect(val)
       , 'expected #{this} to not include ' + _.inspect(val));
   }
@@ -850,6 +887,13 @@ module.exports = function (chai, _) {
    *     expect(obj).to.not.have.property('foo', 'baz');
    *     expect(obj).to.not.have.property('baz', 'bar');
    *
+   * If the `deep` flag is set, asserts that the value of the property is deeply
+   * equal to `value`.
+   *
+   *     var obj = { foo: { bar: 'baz' } };
+   *     expect(obj).to.have.deep.property('foo', { bar: 'baz' });
+   *     expect(obj).to.not.have.deep.property('foo', { bar: 'quux' });
+   *
    * If the `nested` flag is set, you can use dot- and bracket-notation for
    * nested references into objects and arrays.
    *
@@ -861,6 +905,11 @@ module.exports = function (chai, _) {
    *     expect(deepObj).to.have.nested.property('teas[1]', 'matcha');
    *     expect(deepObj).to.have.nested.property('teas[2].tea', 'konacha');
    *
+   * The `deep` and `nested` flags can be combined.
+   *
+   *    expect({ foo: { bar: { baz: 'quux' } } })
+   *      .to.have.deep.nested.property('foo.bar', { baz: 'quux' });
+   *
    * You can also use an array as the starting point of a `nested.property`
    * assertion, or traverse nested arrays.
    *
@@ -900,6 +949,7 @@ module.exports = function (chai, _) {
    *     expect(deepCss).to.have.nested.property('\\.link.\\[target\\]', 42);
    *
    * @name property
+   * @alias deep.property
    * @alias nested.property
    * @param {String} name
    * @param {Mixed} value (optional)
@@ -913,7 +963,10 @@ module.exports = function (chai, _) {
     if (msg) flag(this, 'message', msg);
 
     var isNested = !!flag(this, 'nested')
-      , descriptor = isNested ? 'nested property ' : 'property '
+      , isDeep = !!flag(this, 'deep')
+      , descriptor = (isDeep ? 'deep ' : '')
+                   + (isNested ? 'nested ' : '')
+                   + 'property '
       , negate = flag(this, 'negate')
       , obj = flag(this, 'object')
       , pathInfo = isNested ? _.getPathInfo(name, obj) : null
@@ -938,7 +991,7 @@ module.exports = function (chai, _) {
 
     if (arguments.length > 1) {
       this.assert(
-          hasProperty && val === value
+          hasProperty && (isDeep ? _.eql(val, value) : val === value)
         , 'expected #{this} to have a ' + descriptor + _.inspect(name) + ' of #{exp}, but got #{act}'
         , 'expected #{this} to not have a ' + descriptor + _.inspect(name) + ' of #{act}'
         , val
diff --git a/lib/chai/interface/assert.js b/lib/chai/interface/assert.js
index 1e29514..3f78050 100644
--- a/lib/chai/interface/assert.js
+++ b/lib/chai/interface/assert.js
@@ -826,11 +826,25 @@ module.exports = function (chai, util) {
   /**
    * ### .include(haystack, needle, [message])
    *
-   * Asserts that `haystack` includes `needle`. Works
-   * for strings and arrays.
-   *
-   *     assert.include('foobar', 'bar', 'foobar contains string "bar"');
-   *     assert.include([ 1, 2, 3 ], 3, 'array contains value');
+   * Asserts that `haystack` includes `needle`. Can be used to assert the
+   * inclusion of a value in an array, a substring in a string, or a subset of
+   * properties in an object.
+   *
+   *     assert.include([1,2,3], 2, 'array contains value');
+   *     assert.include('foobar', 'foo', 'string contains substring');
+   *     assert.include({ foo: 'bar', hello: 'universe' }, { foo: 'bar' }, 'object contains property');
+   *
+   * Strict equality (===) is used. When asserting the inclusion of a value in
+   * an array, the array is searched for an element that's strictly equal to the
+   * given value. When asserting a subset of properties in an object, the object
+   * is searched for the given property keys, checking that each one is present
+   * and stricty equal to the given property value. For instance:
+   *
+   *     var obj1 = {a: 1}
+   *       , obj2 = {b: 2};
+   *     assert.include([obj1, obj2], obj1);
+   *     assert.include({foo: obj1, bar: obj2}, {foo: obj1});
+   *     assert.include({foo: obj1, bar: obj2}, {foo: obj1, bar: obj2});
    *
    * @name include
    * @param {Array|String} haystack
@@ -847,11 +861,26 @@ module.exports = function (chai, util) {
   /**
    * ### .notInclude(haystack, needle, [message])
    *
-   * Asserts that `haystack` does not include `needle`. Works
-   * for strings and arrays.
+   * Asserts that `haystack` does not include `needle`. Can be used to assert
+   * the absence of a value in an array, a substring in a string, or a subset of
+   * properties in an object.
+   *
+   *     assert.notInclude([1,2,3], 4, 'array doesn't contain value');
+   *     assert.notInclude('foobar', 'baz', 'string doesn't contain substring');
+   *     assert.notInclude({ foo: 'bar', hello: 'universe' }, { foo: 'baz' }, 'object doesn't contain property');
+   *
+   * Strict equality (===) is used. When asserting the absence of a value in an
+   * array, the array is searched to confirm the absence of an element that's
+   * strictly equal to the given value. When asserting a subset of properties in
+   * an object, the object is searched to confirm that at least one of the given
+   * property keys is either not present or not strictly equal to the given
+   * property value. For instance:
    *
-   *     assert.notInclude('foobar', 'baz', 'string not include substring');
-   *     assert.notInclude([ 1, 2, 3 ], 4, 'array not include contain value');
+   *     var obj1 = {a: 1}
+   *       , obj2 = {b: 2};
+   *     assert.notInclude([obj1, obj2], {a: 1});
+   *     assert.notInclude({foo: obj1, bar: obj2}, {foo: {a: 1}});
+   *     assert.notInclude({foo: obj1, bar: obj2}, {foo: obj1, bar: {b: 2}});
    *
    * @name notInclude
    * @param {Array|String} haystack
@@ -1024,6 +1053,50 @@ module.exports = function (chai, util) {
     new Assertion(obj, msg).to.not.have.property(prop, val);
   };
 
+  /**
+   * ### .deepPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a property named by `property` with a value given
+   * by `value`. Uses a deep equality check.
+   *
+   *     assert.deepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'matcha' });
+   *
+   * @name deepPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.deepPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg).to.have.deep.property(prop, val);
+  };
+
+  /**
+   * ### .notDeepPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a property named by `property` with
+   * value given by `value`. Uses a deep equality check.
+   *
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { black: 'matcha' });
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'oolong' });
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'coffee', { green: 'matcha' });
+   *
+   * @name notDeepPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notDeepPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg).to.not.have.deep.property(prop, val);
+  };
+
   /**
    * ### .nestedPropertyVal(object, property, value, [message])
    *
@@ -1069,6 +1142,52 @@ module.exports = function (chai, util) {
     new Assertion(obj, msg).to.not.have.nested.property(prop, val);
   };
 
+  /**
+   * ### .deepNestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a property named by `property` with a value given
+   * by `value`. `property` can use dot- and bracket-notation for nested
+   * reference. Uses a deep equality check.
+   *
+   *     assert.deepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { matcha: 'yum' });
+   *
+   * @name deepNestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.deepNestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg).to.have.deep.nested.property(prop, val);
+  };
+
+  /**
+   * ### .notDeepNestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a property named by `property` with
+   * value given by `value`. `property` can use dot- and bracket-notation for
+   * nested reference. Uses a deep equality check.
+   *
+   *     assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { oolong: 'yum' });
+   *     assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { matcha: 'yuck' });
+   *     assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.black', { matcha: 'yum' });
+   *
+   * @name notDeepNestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notDeepNestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg).to.not.have.deep.nested.property(prop, val);
+  }
+
   /**
    * ### .lengthOf(object, length, [message])
    *


Test patch:
diff --git a/test/assert.js b/test/assert.js
index a9c7464..a968893 100644
--- a/test/assert.js
+++ b/test/assert.js
@@ -459,7 +459,12 @@ describe('assert', function () {
     assert.include('foobar', 'bar');
     assert.include('', '');
     assert.include([ 1, 2, 3], 3);
-    assert.include({a:1, b:2}, {b:2});
+
+    var obj1 = {a: 1}
+      , obj2 = {b: 2};
+    assert.include([obj1, obj2], obj1);
+    assert.include({foo: obj1, bar: obj2}, {foo: obj1});
+    assert.include({foo: obj1, bar: obj2}, {foo: obj1, bar: obj2});
 
     if (typeof Symbol === 'function') {
       var sym1 = Symbol()
@@ -471,6 +476,14 @@ describe('assert', function () {
       assert.include('foobar', 'baz');
     }, "expected \'foobar\' to include \'baz\'");
 
+    err(function () {
+      assert.include([{a: 1}, {b: 2}], {a: 1});
+    }, "expected [ { a: 1 }, { b: 2 } ] to include { a: 1 }");
+
+    err(function () {
+      assert.include({foo: {a: 1}, bar: {b: 2}}, {foo: {a: 1}});
+    }, "expected { foo: { a: 1 }, bar: { b: 2 } } to have a property 'foo' of { a: 1 }, but got { a: 1 }");
+
     err(function(){
       assert.include(true, true);
     }, "object tested must be an array, an object, or a string, but boolean given");
@@ -492,6 +505,12 @@ describe('assert', function () {
     assert.notInclude('foobar', 'baz');
     assert.notInclude([ 1, 2, 3 ], 4);
 
+    var obj1 = {a: 1}
+      , obj2 = {b: 2};
+    assert.notInclude([obj1, obj2], {a: 1});
+    assert.notInclude({foo: obj1, bar: obj2}, {foo: {a: 1}});
+    assert.notInclude({foo: obj1, bar: obj2}, {foo: obj1, bar: {b: 2}});
+
     if (typeof Symbol === 'function') {
       var sym1 = Symbol()
         , sym2 = Symbol()
@@ -499,6 +518,18 @@ describe('assert', function () {
       assert.notInclude([sym1, sym2], sym3);
     }
 
+    err(function () {
+      var obj1 = {a: 1}
+        , obj2 = {b: 2};
+      assert.notInclude([obj1, obj2], obj1);
+    }, "expected [ { a: 1 }, { b: 2 } ] to not include { a: 1 }");
+
+    err(function () {
+      var obj1 = {a: 1}
+        , obj2 = {b: 2};
+      assert.notInclude({foo: obj1, bar: obj2}, {foo: obj1, bar: obj2});
+    }, "expected { foo: { a: 1 }, bar: { b: 2 } } to not have a property 'foo' of { a: 1 }");
+
     err(function(){
       assert.notInclude(true, true);
     }, "object tested must be an array, an object, or a string, but boolean given");
@@ -947,6 +978,7 @@ describe('assert', function () {
     assert.notProperty(obj, 'foo.bar');
     assert.notPropertyVal(simpleObj, 'foo', 'flow');
     assert.notPropertyVal(simpleObj, 'flow', 'bar');
+    assert.notPropertyVal(obj, 'foo', {bar: 'baz'});
     assert.notNestedProperty(obj, 'foo.baz');
     assert.nestedPropertyVal(obj, 'foo.bar', 'baz');
     assert.notNestedPropertyVal(obj, 'foo.bar', 'flow');
@@ -989,6 +1021,46 @@ describe('assert', function () {
     }, "expected { foo: { bar: 'baz' } } to not have a nested property 'foo.bar' of 'baz'");
   });
 
+  it('deepPropertyVal', function () {
+    var obj = {a: {b: 1}};
+    assert.deepPropertyVal(obj, 'a', {b: 1});
+    assert.notDeepPropertyVal(obj, 'a', {b: 7});
+    assert.notDeepPropertyVal(obj, 'a', {z: 1});
+    assert.notDeepPropertyVal(obj, 'z', {b: 1});
+
+    err(function () {
+      assert.deepPropertyVal(obj, 'a', {b: 7}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'a' of { b: 7 }, but got { b: 1 }");
+
+    err(function () {
+      assert.deepPropertyVal(obj, 'z', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'z'");
+
+    err(function () {
+      assert.notDeepPropertyVal(obj, 'a', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to not have a deep property 'a' of { b: 1 }");
+  });
+
+  it('deepNestedPropertyVal', function () {
+    var obj = {a: {b: {c: 1}}};
+    assert.deepNestedPropertyVal(obj, 'a.b', {c: 1});
+    assert.notDeepNestedPropertyVal(obj, 'a.b', {c: 7});
+    assert.notDeepNestedPropertyVal(obj, 'a.b', {z: 1});
+    assert.notDeepNestedPropertyVal(obj, 'a.z', {c: 1});
+
+    err(function () {
+      assert.deepNestedPropertyVal(obj, 'a.b', {c: 7}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.b' of { c: 7 }, but got { c: 1 }");
+
+    err(function () {
+      assert.deepNestedPropertyVal(obj, 'a.z', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.z'");
+
+    err(function () {
+      assert.notDeepNestedPropertyVal(obj, 'a.b', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to not have a deep nested property 'a.b' of { c: 1 }");
+  });
+
   it('throws / throw / Throw', function() {
     ['throws', 'throw', 'Throw'].forEach(function (throws) {
       assert[throws](function() { throw new Error('foo'); });
diff --git a/test/expect.js b/test/expect.js
index 4f01053..29d91f2 100644
--- a/test/expect.js
+++ b/test/expect.js
@@ -534,6 +534,7 @@ describe('expect', function () {
     expect('asd').to.have.property('constructor', String);
     expect('test').to.not.have.property('length', 3);
     expect('test').to.not.have.property('foo', 4);
+    expect({a: {b: 1}}).to.not.have.property('a', {b: 1});
 
     var deepObj = {
         green: { tea: 'matcha' }
@@ -589,6 +590,26 @@ describe('expect', function () {
     }, "blah: expected 'asd' to have a property 'constructor' of [Function: Number], but got [Function: String]");
   });
 
+  it('deep.property(name, val)', function () {
+    var obj = {a: {b: 1}};
+    expect(obj).to.have.deep.property('a', {b: 1});
+    expect(obj).to.not.have.deep.property('a', {b: 7});
+    expect(obj).to.not.have.deep.property('a', {z: 1});
+    expect(obj).to.not.have.deep.property('z', {b: 1});
+
+    err(function () {
+      expect(obj).to.have.deep.property('a', {b: 7}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'a' of { b: 7 }, but got { b: 1 }");
+
+    err(function () {
+      expect(obj).to.have.deep.property('z', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'z'");
+
+    err(function () {
+      expect(obj).to.not.have.deep.property('a', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to not have a deep property 'a' of { b: 1 }");
+  });
+  
   it('nested.property(name, val)', function(){
     expect({ foo: { bar: 'baz' } })
       .to.have.nested.property('foo.bar', 'baz');
@@ -596,6 +617,7 @@ describe('expect', function () {
       .to.not.have.nested.property('foo.bar', 'quux');
     expect({ foo: { bar: 'baz' } })
       .to.not.have.nested.property('foo.quux', 'baz');
+    expect({a: {b: {c: 1}}}).to.not.have.nested.property('a.b', {c: 1});
 
     err(function(){
       expect({ foo: { bar: 'baz' } })
@@ -607,6 +629,26 @@ describe('expect', function () {
     }, "blah: expected { foo: { bar: 'baz' } } to not have a nested property 'foo.bar' of 'baz'");
   });
 
+  it('deep.nested.property(name, val)', function () {
+    var obj = {a: {b: {c: 1}}};
+    expect(obj).to.have.deep.nested.property('a.b', {c: 1});
+    expect(obj).to.not.have.deep.nested.property('a.b', {c: 7});
+    expect(obj).to.not.have.deep.nested.property('a.b', {z: 1});
+    expect(obj).to.not.have.deep.nested.property('a.z', {c: 1});
+
+    err(function () {
+      expect(obj).to.have.deep.nested.property('a.b', {c: 7}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.b' of { c: 7 }, but got { c: 1 }");
+
+    err(function () {
+      expect(obj).to.have.deep.nested.property('a.z', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.z'");
+
+    err(function () {
+      expect(obj).to.not.have.deep.nested.property('a.b', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to not have a deep nested property 'a.b' of { c: 1 }");
+  });
+
   it('ownProperty(name)', function(){
     expect('test').to.have.ownProperty('length');
     expect('test').to.haveOwnProperty('length');
@@ -680,14 +722,15 @@ describe('expect', function () {
     expect([1,2]).to.include(1);
     expect(['foo', 'bar']).to.not.include('baz');
     expect(['foo', 'bar']).to.not.include(1);
-    expect({a:1,b:2}).to.include({b:2});
-    expect({a:1,b:2}).to.not.include({b:3});
-    expect({a:1,b:2}).to.include({a:1,b:2});
-    expect({a:1,b:2}).to.not.include({a:1,c:2});
 
-    expect([{a:1},{b:2}]).to.include({a:1});
-    expect([{a:1}]).to.include({a:1});
-    expect([{a:1}]).to.not.include({b:1});
+    var obj1 = {a: 1}
+      , obj2 = {b: 2};
+    expect([obj1, obj2]).to.include(obj1);
+    expect([obj1, obj2]).to.not.include({a: 1});
+    expect({foo: obj1, bar: obj2}).to.include({foo: obj1});
+    expect({foo: obj1, bar: obj2}).to.include({foo: obj1, bar: obj2});
+    expect({foo: obj1, bar: obj2}).to.not.include({foo: {a: 1}});
+    expect({foo: obj1, bar: obj2}).to.not.include({foo: obj1, bar: {b: 2}});
 
     if (typeof Symbol === 'function') {
       var sym1 = Symbol()
@@ -711,11 +754,27 @@ describe('expect', function () {
 
     err(function(){
       expect({a:1,b:2}).to.not.include({b:2});
-    }, "expected { a: 1, b: 2 } to not include { b: 2 }");
+    }, "expected { a: 1, b: 2 } to not have a property 'b' of 2");
 
-    err(function(){
-      expect([{a:1},{b:2}]).to.not.include({b:2});
-    }, "expected [ { a: 1 }, { b: 2 } ] to not include { b: 2 }");
+    err(function () {
+      expect([{a: 1}, {b: 2}]).to.include({a: 1});
+    }, "expected [ { a: 1 }, { b: 2 } ] to include { a: 1 }");
+
+    err(function () {
+      var obj1 = {a: 1}
+        , obj2 = {b: 2};
+      expect([obj1, obj2]).to.not.include(obj1);
+    }, "expected [ { a: 1 }, { b: 2 } ] to not include { a: 1 }");
+
+    err(function () {
+      expect({foo: {a: 1}, bar: {b: 2}}).to.include({foo: {a: 1}});
+    }, "expected { foo: { a: 1 }, bar: { b: 2 } } to have a property 'foo' of { a: 1 }, but got { a: 1 }");
+
+    err(function () {
+      var obj1 = {a: 1}
+        , obj2 = {b: 2};
+      expect({foo: obj1, bar: obj2}).to.not.include({foo: obj1, bar: obj2});
+    }, "expected { foo: { a: 1 }, bar: { b: 2 } } to not have a property 'foo' of { a: 1 }");
 
     err(function(){
       expect(true).to.include(true);
diff --git a/test/should.js b/test/should.js
index e369b29..345af84 100644
--- a/test/should.js
+++ b/test/should.js
@@ -457,6 +457,7 @@ describe('should', function() {
     ({ 1: 1 }).should.have.property(1, 1);
     'test'.should.not.have.property('length', 3);
     'test'.should.not.have.property('foo', 4);
+    ({a: {b: 1}}).should.not.have.property('a', {b: 1});
 
     err(function(){
       'asd'.should.have.property('length', 4, 'blah');
@@ -471,10 +472,31 @@ describe('should', function() {
     }, "blah: expected 'asd' to have a property 'constructor' of [Function: Number], but got [Function: String]");
   });
 
+  it('deep.property(name, val)', function () {
+    var obj = {a: {b: 1}};
+    obj.should.have.deep.property('a', {b: 1});
+    obj.should.not.have.deep.property('a', {b: 7});
+    obj.should.not.have.deep.property('a', {z: 1});
+    obj.should.not.have.deep.property('z', {b: 1});
+
+    err(function () {
+      obj.should.have.deep.property('a', {b: 7}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'a' of { b: 7 }, but got { b: 1 }");
+
+    err(function () {
+      obj.should.have.deep.property('z', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'z'");
+
+    err(function () {
+      obj.should.not.have.deep.property('a', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to not have a deep property 'a' of { b: 1 }");
+  });
+
   it('nested.property(name, val)', function(){
     ({ foo: { bar: 'baz' } }).should.have.nested.property('foo.bar', 'baz');
     ({ foo: { bar: 'baz' } }).should.not.have.nested.property('foo.bar', 'quux');
     ({ foo: { bar: 'baz' } }).should.not.have.nested.property('foo.quux', 'baz');
+    ({a: {b: {c: 1}}}).should.not.have.nested.property('a.b', {c: 1});
 
     err(function(){
       ({ foo: { bar: 'baz' } }).should.have.nested.property('foo.bar', 'quux', 'blah');
@@ -484,6 +506,26 @@ describe('should', function() {
     }, "blah: expected { foo: { bar: 'baz' } } to not have a nested property 'foo.bar' of 'baz'");
   });
 
+  it('deep.nested.property(name, val)', function () {
+    var obj = {a: {b: {c: 1}}};
+    obj.should.have.deep.nested.property('a.b', {c: 1});
+    obj.should.not.have.deep.nested.property('a.b', {c: 7});
+    obj.should.not.have.deep.nested.property('a.b', {z: 1});
+    obj.should.not.have.deep.nested.property('a.z', {c: 1});
+
+    err(function () {
+      obj.should.have.deep.nested.property('a.b', {c: 7}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.b' of { c: 7 }, but got { c: 1 }");
+
+    err(function () {
+      obj.should.have.deep.nested.property('a.z', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.z'");
+
+    err(function () {
+      obj.should.not.have.deep.nested.property('a.b', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to not have a deep nested property 'a.b' of { c: 1 }");
+  });
+
   it('ownProperty(name)', function(){
     'test'.should.have.ownProperty('length');
     'test'.should.haveOwnProperty('length');
@@ -572,8 +614,15 @@ describe('should', function() {
     [1,2].should.include(1);
     ['foo', 'bar'].should.not.include('baz');
     ['foo', 'bar'].should.not.include(1);
-    ({a:1,b:2}).should.include({b:2});
-    ({a:1,b:2}).should.not.include({b:3});
+
+    var obj1 = {a: 1}
+      , obj2 = {b: 2};
+    [obj1, obj2].should.include(obj1);
+    [obj1, obj2].should.not.include({a: 1});
+    ({foo: obj1, bar: obj2}).should.include({foo: obj1});
+    ({foo: obj1, bar: obj2}).should.include({foo: obj1, bar: obj2});
+    ({foo: obj1, bar: obj2}).should.not.include({foo: {a: 1}});
+    ({foo: obj1, bar: obj2}).should.not.include({foo: obj1, bar: {b: 2}});
 
     if (typeof Symbol === 'function') {
       var sym1 = Symbol()
@@ -595,6 +644,26 @@ describe('should', function() {
       ({a:1}).should.include({b:2});
     }, "expected { a: 1 } to have a property 'b'");
 
+    err(function () {
+      [{a: 1}, {b: 2}].should.include({a: 1});
+    }, "expected [ { a: 1 }, { b: 2 } ] to include { a: 1 }");
+
+    err(function () {
+      var obj1 = {a: 1}
+        , obj2 = {b: 2};
+      [obj1, obj2].should.not.include(obj1);
+    }, "expected [ { a: 1 }, { b: 2 } ] to not include { a: 1 }");
+
+    err(function () {
+      ({foo: {a: 1}, bar: {b: 2}}).should.include({foo: {a: 1}});
+    }, "expected { foo: { a: 1 }, bar: { b: 2 } } to have a property 'foo' of { a: 1 }, but got { a: 1 }");
+
+    err(function () {
+      var obj1 = {a: 1}
+        , obj2 = {b: 2};
+      ({foo: obj1, bar: obj2}).should.not.include({foo: obj1, bar: obj2});
+    }, "expected { foo: { a: 1 }, bar: { b: 2 } } to not have a property 'foo' of { a: 1 }");
+
     err(function(){
       (true).should.include(true);
     }, "object tested must be an array, an object, or a string, but boolean given");