Repository: chaijs/chai
Pull request #633 (2016-03-07 20:51:55)

Keys for maps and sets
Hello everyone, this PR aims to solve #632.

Here's a summary of the changes I made:

1. I am using `.some()` instead of `.filter()` when checking for `any` keys. There is no need to calculate the full size of intersections (`intersection > 0`), if we have one it's already bigger than `0` therefore we don't need to iterate through everything.
2. I had to change the `mixedArgs` message because **`keys` assertion now accepts objects as keys** (as I've said on #632).
3. When checking maps and sets' keys we need to compare objects, that's why I'm using `_.eql(expectedKey, actualKey)`.
4. I noticed that the `assert` interface had no `keys` related methods, so I added every possible combination (any/all/contains/have - Please notice that some of these combinations have the same meaning, as explained on the `keys` method description, so I added only the ones with different effects)
5. I included tests for these changes on all three interface's test files.
6. Every method I've touched got it's JSDoc comments updated (examples were included too).

PS.: When creating these tests I've had to use `if (Maps !== undefined)` and `if (Sets !== undefined)` because apparently PhantomJS 1.9 uses an engine that hasn't implemented these specs yet.

Feel free to point possible mistakes and give feedback or new ideas.

Thanks everyone

keithamus: Only a couple of minor points above. Otherwise this looks _awesome_ @lucasfcosta. Great work, as usual :smile: 
lucasfcosta: Thanks, @keithamus!
Your feedback is always valuable. :smile: 

Well, I'm aware `_.eql` uses deep equality and I've used it because when, for example, a Map has an object as Key and I compare the objects (even if they're equal) I will get `false` as result because `===` checks for referential equality, therefore this, for example, wouldn't work:

```js
expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.key([{objKey: 'value'}]);
```

This will fail, because although the object used as key on the map is depply equal to the one passed to the `.key` assertion they're not the same so `===` will return false. If you want to check for referential equality only then I feel like we should use `===`.
keithamus: @lucasfcosta sure, I expected you to say as much - I should have elaborated more in my original comment, but got a little side tracked with other things.

Deep equality is going to be _sometimes_ desirable within Maps, but _sometimes_ referential equality will be desirable. If we start with using referential equality, we can always move to deep equality later - by adding the `.deep` qualifier. We won't be able to (easily) do the same the other way around.
lucasfcosta: Ah, I understand now and I totally agree, it really seems to be the right step. I haven't thought about it before.
I can also implement the deep check when there is a `deep` flag if you want me to.

Thanks for the explanation, I'll make sure to update this PR with the changes we've talked about ASAP :smiley: 
keithamus: We can tackle the deep flag in another PR, to keep this one simple :smile: 

Patch:
diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js
index 68f8c27..5b80087 100644
--- a/lib/chai/core/assertions.js
+++ b/lib/chai/core/assertions.js
@@ -1111,7 +1111,6 @@ module.exports = function (chai, _) {
     );
   });
 
-
   /**
    * ### .keys(key1, [key2], [...])
    *
@@ -1133,6 +1132,10 @@ module.exports = function (chai, _) {
    * match the number of keys passed in (in other words, a target object must
    * have all and only all of the passed-in keys).
    *
+   * You can also use this on Sets and Maps. Please notice that these two types
+   * can have objects as keys, so, in this case, you can pass multiple objects as arguments if
+   * you want to.
+   *
    *     expect({ foo: 1, bar: 2 }).to.have.any.keys('foo', 'baz');
    *     expect({ foo: 1, bar: 2 }).to.have.any.keys('foo');
    *     expect({ foo: 1, bar: 2 }).to.contain.any.keys('bar', 'baz');
@@ -1142,7 +1145,10 @@ module.exports = function (chai, _) {
    *     expect({ foo: 1, bar: 2 }).to.have.all.keys({'bar': 6, 'foo': 7});
    *     expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys(['bar', 'foo']);
    *     expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys({'bar': 6});
-   *
+   *     expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.key({objKey: 'value'});
+   *     expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.any.keys([{objKey: 'value'}, {anotherKey: 'anotherValue'}]);
+   *     expect(new Map([['firstKey', 'firstValue'], [1, 2]])).to.contain.all.keys('firstKey', 1);
+   *     expect(new Set([['foo', 'bar'], ['example', 1]])).to.have.any.keys('foo');
    *
    * @name keys
    * @alias key
@@ -1155,29 +1161,40 @@ module.exports = function (chai, _) {
     var obj = flag(this, 'object')
       , str
       , ok = true
-      , mixedArgsMsg = 'keys must be given single argument of Array|Object|String, or multiple String arguments';
-
-    switch (_.type(keys)) {
-      case "array":
-        if (arguments.length > 1) throw (new Error(mixedArgsMsg));
-        break;
-      case "object":
-        if (arguments.length > 1) throw (new Error(mixedArgsMsg));
-        keys = Object.keys(keys);
-        break;
-      default:
+      , mixedArgsMsg = 'when testing keys against an object or an array you must give a single Array|Object|String argument or multiple String arguments';
+
+    if (_.type(obj) === 'map' || _.type(obj) === 'set') {
+      actual = Array.from(obj.keys());
+
+      if (_.type(keys) !== 'array') {
         keys = Array.prototype.slice.call(arguments);
+      }
+
+    } else {
+      actual = Object.keys(obj);
+
+      switch (_.type(keys)) {
+        case 'array':
+          if (arguments.length > 1) throw new Error(mixedArgsMsg);
+          break;
+        case 'object':
+          if (arguments.length > 1) throw new Error(mixedArgsMsg);
+          keys = Object.keys(keys);
+          break;
+        default:
+          keys = Array.prototype.slice.call(arguments);
+      }
+
+      keys = keys.map(String);
     }
 
     if (!keys.length) throw new Error('keys required');
 
-    keys = keys.map(String);
-
-    var actual = Object.keys(obj)
-      , expected = keys
-      , len = keys.length
+    var len = keys.length
       , any = flag(this, 'any')
-      , all = flag(this, 'all');
+      , all = flag(this, 'all')
+      , expected = keys
+      , actual;
 
     if (!any && !all) {
       all = true;
@@ -1185,17 +1202,21 @@ module.exports = function (chai, _) {
 
     // Has any
     if (any) {
-      var intersection = expected.filter(function(key) {
-        return ~actual.indexOf(key);
+      ok = expected.some(function(expectedKey) {
+        return actual.some(function(actualKey) {
+          return expectedKey === actualKey;
+        });
       });
-      ok = intersection.length > 0;
     }
 
     // Has all
     if (all) {
-      ok = keys.every(function(key){
-        return ~actual.indexOf(key);
+      ok = expected.every(function(expectedKey) {
+        return actual.some(function(actualKey) {
+          return expectedKey === actualKey;
+        });
       });
+
       if (!flag(this, 'negate') && !flag(this, 'contains')) {
         ok = ok && keys.length == actual.length;
       }
@@ -1203,7 +1224,7 @@ module.exports = function (chai, _) {
 
     // Key string
     if (len > 1) {
-      keys = keys.map(function(key){
+      keys = keys.map(function(key) {
         return _.inspect(key);
       });
       var last = keys.pop();
@@ -1809,7 +1830,7 @@ module.exports = function (chai, _) {
     var realDelta = flag(this, 'realDelta');
 
     var expression;
-    if (behavior === 'change') {  
+    if (behavior === 'change') {
       expression = Math.abs(final - initial) === Math.abs(delta);
     } else {
       expression = realDelta === Math.abs(delta);
diff --git a/lib/chai/interface/assert.js b/lib/chai/interface/assert.js
index 8fd3d91..5b44b5c 100644
--- a/lib/chai/interface/assert.js
+++ b/lib/chai/interface/assert.js
@@ -1086,6 +1086,150 @@ module.exports = function (chai, util) {
     new Assertion(exp, msg).to.have.length(len);
   };
 
+  /**
+   * ### .hasAnyKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has at least one of the `keys` provided.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.hasAnyKey({foo: 1, bar: 2, baz: 3}, ['foo', 'iDontExist', 'baz']);
+   *     assert.hasAnyKey({foo: 1, bar: 2, baz: 3}, {foo: 30, iDontExist: 99, baz: 1337]);
+   *     assert.hasAnyKey(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{foo: 1}, 'thisKeyDoesNotExist']);
+   *     assert.hasAnyKey(new Set([{foo: 'bar'}, 'anotherKey'], [{foo: 'bar'}, 'thisKeyDoesNotExist']);
+   *
+   * @name hasAnyKeys
+   * @param {Mixed} object
+   * @param {Array|Object} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.hasAnyKeys = function (obj, keys, msg) {
+    if (keys === undefined) {
+      new Assertion(obj, msg).to.not.have.all.keys();
+    }
+
+    new Assertion(obj, msg).to.have.any.keys(keys);
+  }
+
+  /**
+   * ### .hasAllKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has all and only all of the `keys` provided.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.hasAllKeys({foo: 1, bar: 2, baz: 3}, ['foo', 'bar', 'baz']);
+   *     assert.hasAllKeys({foo: 1, bar: 2, baz: 3}, {foo: 30, bar: 99, baz: 1337]);
+   *     assert.hasAllKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{foo: 1}, 'key']);
+   *     assert.hasAllKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{foo: 'bar'}, 'anotherKey']);
+   *
+   * @name hasAllKeys
+   * @param {Mixed} object
+   * @param {String[]} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.hasAllKeys = function (obj, keys, msg) {
+    if (keys === undefined) {
+      new Assertion(obj, msg).to.not.have.all.keys();
+    }
+
+    new Assertion(obj, msg).to.have.all.keys(keys);
+  }
+
+  /**
+   * ### .containsAllKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has all of the `keys` provided but may have more keys not listed.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.containsAllKeys({foo: 1, bar: 2, baz: 3}, ['foo', 'baz']);
+   *     assert.containsAllKeys({foo: 1, bar: 2, baz: 3}, ['foo', 'bar', 'baz']);
+   *     assert.containsAllKeys({foo: 1, bar: 2, baz: 3}, {foo: 30, baz: 1337});
+   *     assert.containsAllKeys({foo: 1, bar: 2, baz: 3}, {foo: 30, bar: 99, baz: 1337});
+   *     assert.containsAllKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{foo: 1}]);
+   *     assert.containsAllKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{foo: 1}, 'key']);
+   *     assert.containsAllKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{foo: 'bar'}]);
+   *     assert.containsAllKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{foo: 'bar'}, 'anotherKey']);
+   *
+   * @name containsAllKeys
+   * @param {Mixed} object
+   * @param {String[]} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.containsAllKeys = function (obj, keys, msg) {
+    if (keys === undefined) {
+      new Assertion(obj, msg).to.not.have.all.keys();
+    }
+
+    new Assertion(obj, msg).to.contain.all.keys(keys);
+  }
+
+  /**
+   * ### .doesNotHaveAnyKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has none of the `keys` provided.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.doesNotHaveAnyKeys({foo: 1, bar: 2, baz: 3}, ['one', 'two', 'example']);
+   *     assert.doesNotHaveAnyKeys({foo: 1, bar: 2, baz: 3}, {one: 1, two: 2, example: 'foo'});
+   *     assert.doesNotHaveAnyKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{one: 'two'}, 'example']);
+   *     assert.doesNotHaveAnyKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{one: 'two'}, 'example']);
+   *
+   * @name doesNotHaveAnyKeys
+   * @param {Mixed} object
+   * @param {String[]} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotHaveAnyKeys = function (obj, keys, msg) {
+    if (keys === undefined) {
+      new Assertion(obj, msg).to.not.have.all.keys();
+    }
+
+    new Assertion(obj, msg).to.not.have.any.keys(keys);
+  }
+
+  /**
+   * ### .doesNotHaveAllKeys(object, [keys], [message])
+   *
+   * Asserts that `object` does not have at least one of the `keys` provided.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.doesNotHaveAllKeys({foo: 1, bar: 2, baz: 3}, ['one', 'two', 'example']);
+   *     assert.doesNotHaveAllKeys({foo: 1, bar: 2, baz: 3}, {one: 1, two: 2, example: 'foo'});
+   *     assert.doesNotHaveAllKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{one: 'two'}, 'example']);
+   *     assert.doesNotHaveAllKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{one: 'two'}, 'example']);
+   *
+   * @name doesNotHaveAllKeys
+   * @param {Mixed} object
+   * @param {String[]} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotHaveAllKeys = function (obj, keys, msg) {
+    if (keys === undefined) {
+      new Assertion(obj, msg).to.not.have.all.keys();
+    }
+
+    new Assertion(obj, msg).to.not.have.all.keys(keys);
+  }
+
   /**
    * ### .throws(function, [constructor/string/regexp], [string/regexp], [message])
    *


Test patch:
diff --git a/test/assert.js b/test/assert.js
index 81ff78f..fcfc4d5 100644
--- a/test/assert.js
+++ b/test/assert.js
@@ -480,6 +480,284 @@ describe('assert', function () {
     }, "expected \'foobar\' to not include \'bar\'");
   });
 
+  it('keys(array|Object|arguments)', function(){
+    assert.hasAllKeys({ foo: 1 }, [ 'foo' ]);
+    assert.hasAllKeys({ foo: 1, bar: 2 }, [ 'foo', 'bar' ]);
+    assert.hasAllKeys({ foo: 1 }, { foo: 30 });
+    assert.hasAllKeys({ foo: 1, bar: 2 }, { 'foo': 6, 'bar': 7 });
+
+    assert.containsAllKeys({ foo: 1, bar: 2, baz: 3 }, [ 'foo', 'bar' ]);
+    assert.containsAllKeys({ foo: 1, bar: 2, baz: 3 }, [ 'bar', 'foo' ]);
+    assert.containsAllKeys({ foo: 1, bar: 2, baz: 3 }, [ 'baz' ]);
+    assert.containsAllKeys({ foo: 1, bar: 2 }, [ 'foo' ]);
+    assert.containsAllKeys({ foo: 1, bar: 2 }, ['bar']);
+    assert.containsAllKeys({ foo: 1, bar: 2 }, { 'foo': 6 });
+    assert.containsAllKeys({ foo: 1, bar: 2 }, { 'bar': 7 });
+    assert.containsAllKeys({ foo: 1, bar: 2 }, { 'foo': 6 });
+    assert.containsAllKeys({ foo: 1, bar: 2 }, { 'bar': 7, 'foo': 6 });
+
+    assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, [ 'baz' ]);
+    assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, [ 'foo', 'baz' ]);
+    assert.doesNotHaveAllKeys({ foo: 1, bar: 2, baz: 3 }, [ 'foo', 'bar', 'baz', 'fake' ]);
+    assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, [ 'baz', 'foo' ]);
+    assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, { 'baz': 8 });
+    assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, { 'baz': 8, 'foo': 7 });
+    assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, { 'baz': 8, 'fake': 7 });
+
+    assert.hasAnyKeys({ foo: 1, bar: 2 }, [ 'foo', 'baz' ]);
+    assert.hasAnyKeys({ foo: 1, bar: 2 }, [ 'foo' ]);
+    assert.hasAnyKeys({ foo: 1, bar: 2 }, [ 'bar', 'baz' ]);
+    assert.hasAnyKeys({ foo: 1, bar: 2 }, [ 'bar', 'foo' ]);
+    assert.hasAnyKeys({ foo: 1, bar: 2 }, [ 'foo', 'bar' ]);
+    assert.hasAnyKeys({ foo: 1, bar: 2 }, [ 'baz', 'fake', 'foo' ]);
+    assert.hasAnyKeys({ foo: 1, bar: 2 }, { 'foo': 6 });
+    assert.hasAnyKeys({ foo: 1, bar: 2 }, { 'baz': 6, 'foo': 12 });
+
+    assert.doesNotHaveAnyKeys({ foo: 1, bar: 2 }, [ 'baz', 'abc', 'def' ]);
+    assert.doesNotHaveAnyKeys({ foo: 1, bar: 2 }, [ 'baz' ]);
+    assert.doesNotHaveAnyKeys({ foo: 1, bar: 2 }, { baz: 1, biz: 2, fake: 3 });
+    assert.doesNotHaveAnyKeys({ foo: 1, bar: 2 }, { baz: 1 });
+
+    if (typeof Map !== 'undefined') {
+      var aKey = {thisIs: 'anExampleObject'};
+      var anotherKey = {doingThisBecauseOf: 'referential equality'};
+
+      assert.hasAnyKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ aKey ]);
+      assert.hasAnyKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ 'thisDoesNotExist', 'thisToo', aKey ]);
+      assert.hasAllKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ aKey, anotherKey ]);
+
+      assert.containsAllKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ aKey ]);
+      assert.doesNotHaveAllKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ aKey, {iDoNot: 'exist'} ]);
+
+      assert.doesNotHaveAnyKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ {iDoNot: 'exist'} ]);
+      assert.doesNotHaveAnyKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ 'thisDoesNotExist', 'thisToo', {iDoNot: 'exist'} ]);
+      assert.doesNotHaveAllKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ 'thisDoesNotExist', 'thisToo', anotherKey ]);
+
+      assert.doesNotHaveAnyKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ {iDoNot: 'exist'}, 'thisDoesNotExist' ]);
+      assert.doesNotHaveAnyKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ 'thisDoesNotExist', 'thisToo', {iDoNot: 'exist'} ]);
+      assert.doesNotHaveAllKeys(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]), [ aKey, {iDoNot: 'exist'} ]);
+
+      err(function(){
+        assert.hasAllKeys(new Map([[{1: 20}, 'number']]));
+      }, "keys required");
+
+      err(function(){
+        assert.hasAllKeys(new Map([[{1: 20}, 'number']]), []);
+      }, "keys required");
+
+      err(function(){
+        assert.containsAllKeys(new Map([[{1: 20}, 'number']]));
+      }, "keys required");
+
+      err(function(){
+        assert.containsAllKeys(new Map([[{1: 20}, 'number']]), []);
+      }, "keys required");
+
+      err(function(){
+        assert.doesNotHaveAllKeys(new Map([[{1: 20}, 'number']]));
+      }, "keys required");
+
+      err(function(){
+        assert.doesNotHaveAllKeys(new Map([[{1: 20}, 'number']]), []);
+      }, "keys required");
+
+      err(function(){
+        assert.hasAnyKeys(new Map([[{1: 20}, 'number']]));
+      }, "keys required");
+
+      err(function(){
+        assert.hasAnyKeys(new Map([[{1: 20}, 'number']]), []);
+      }, "keys required");
+
+      err(function(){
+        assert.doesNotHaveAnyKeys(new Map([[{1: 20}, 'number']]));
+      }, "keys required");
+
+      err(function(){
+        assert.doesNotHaveAnyKeys(new Map([[{1: 20}, 'number']]), []);
+      }, "keys required");
+    }
+
+    if (typeof Set !== 'undefined') {
+      var aKey = {thisIs: 'anExampleObject'};
+      var anotherKey = {doingThisBecauseOf: 'referential equality'};
+
+      assert.hasAnyKeys(new Set([aKey, anotherKey]), [ aKey ]);
+      assert.hasAnyKeys(new Set([aKey, anotherKey]), [ 20, 1, aKey ]);
+      assert.hasAllKeys(new Set([aKey, anotherKey]), [ aKey, anotherKey ]);
+
+      assert.containsAllKeys(new Set([aKey, anotherKey]), [ aKey ]);
+      assert.doesNotHaveAllKeys(new Set([aKey, anotherKey]), [ aKey, {iDoNot: 'exist'} ]);
+
+      assert.doesNotHaveAnyKeys(new Set([aKey, anotherKey]), [ {iDoNot: 'exist'} ]);
+      assert.doesNotHaveAnyKeys(new Set([aKey, anotherKey]), [ 'thisDoesNotExist', 'thisToo', {iDoNot: 'exist'} ]);
+      assert.doesNotHaveAllKeys(new Set([aKey, anotherKey]), [ 'thisDoesNotExist', 'thisToo', anotherKey ]);
+
+      assert.doesNotHaveAnyKeys(new Set([aKey, anotherKey]), [ {iDoNot: 'exist'}, 'thisDoesNotExist' ]);
+      assert.doesNotHaveAnyKeys(new Set([aKey, anotherKey]), [ 20, 1, {iDoNot: 'exist'} ]);
+      assert.doesNotHaveAllKeys(new Set([aKey, anotherKey]), [ 'thisDoesNotExist', 'thisToo', {iDoNot: 'exist'} ]);
+
+      err(function(){
+        assert.hasAllKeys(new Set([{1: 20}, 'number']));
+      }, "keys required");
+
+      err(function(){
+        assert.hasAllKeys(new Set([{1: 20}, 'number']), []);
+      }, "keys required");
+
+      err(function(){
+        assert.containsAllKeys(new Set([{1: 20}, 'number']));
+      }, "keys required");
+
+      err(function(){
+        assert.containsAllKeys(new Set([{1: 20}, 'number']), []);
+      }, "keys required");
+
+      err(function(){
+        assert.doesNotHaveAllKeys(new Set([{1: 20}, 'number']));
+      }, "keys required");
+
+      err(function(){
+        assert.doesNotHaveAllKeys(new Set([{1: 20}, 'number']), []);
+      }, "keys required");
+
+      err(function(){
+        assert.hasAnyKeys(new Set([{1: 20}, 'number']));
+      }, "keys required");
+
+      err(function(){
+        assert.hasAnyKeys(new Set([{1: 20}, 'number']), []);
+      }, "keys required");
+
+      err(function(){
+        assert.doesNotHaveAnyKeys(new Set([{1: 20}, 'number']));
+      }, "keys required");
+
+      err(function(){
+        assert.doesNotHaveAnyKeys(new Set([{1: 20}, 'number']), []);
+      }, "keys required");
+    }
+
+    err(function(){
+      assert.hasAllKeys({ foo: 1 });
+    }, "keys required");
+
+    err(function(){
+      assert.hasAllKeys({ foo: 1 }, []);
+    }, "keys required");
+
+    err(function(){
+      assert.containsAllKeys({ foo: 1 });
+    }, "keys required");
+
+    err(function(){
+      assert.containsAllKeys({ foo: 1 }, []);
+    }, "keys required");
+
+    err(function(){
+      assert.doesNotHaveAllKeys({ foo: 1 });
+    }, "keys required");
+
+    err(function(){
+      assert.doesNotHaveAllKeys({ foo: 1 }, []);
+    }, "keys required");
+
+    err(function(){
+      assert.hasAnyKeys({ foo: 1 });
+    }, "keys required");
+
+    err(function(){
+      assert.hasAnyKeys({ foo: 1 }, []);
+    }, "keys required");
+
+    err(function(){
+      assert.doesNotHaveAnyKeys({ foo: 1 });
+    }, "keys required");
+
+    err(function(){
+      assert.doesNotHaveAnyKeys({ foo: 1 }, []);
+    }, "keys required");
+
+    err(function(){
+      assert.hasAllKeys({ foo: 1 }, ['bar']);
+    }, "expected { foo: 1 } to have key 'bar'");
+
+    err(function(){
+      assert.hasAllKeys({ foo: 1 }, ['bar', 'baz']);
+    }, "expected { foo: 1 } to have keys 'bar', and 'baz'");
+
+    err(function(){
+      assert.hasAllKeys({ foo: 1 }, ['foo', 'bar', 'baz']);
+    }, "expected { foo: 1 } to have keys 'foo', 'bar', and 'baz'");
+
+    err(function(){
+      assert.doesNotHaveAllKeys({ foo: 1 }, ['foo']);
+    }, "expected { foo: 1 } to not have key 'foo'");
+
+    err(function(){
+      assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, ['foo', 'bar']);
+    }, "expected { foo: 1, bar: 2 } to not have keys 'foo', and 'bar'");
+
+    err(function(){
+      assert.hasAllKeys({ foo: 1, bar: 2 }, ['foo']);
+    }, "expected { foo: 1, bar: 2 } to have key 'foo'");
+
+    err(function(){
+      assert.containsAllKeys({ foo: 1 }, ['foo', 'bar']);
+    }, "expected { foo: 1 } to contain keys 'foo', and 'bar'");
+
+    err(function() {
+      assert.hasAnyKeys({ foo: 1 }, ['baz']);
+    }, "expected { foo: 1 } to have key 'baz'");
+
+    err(function(){
+      assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, ['foo', 'bar']);
+    }, "expected { foo: 1, bar: 2 } to not have keys 'foo', and 'bar'");
+
+    err(function(){
+      assert.doesNotHaveAnyKeys({ foo: 1, bar: 2 }, ['foo', 'baz']);
+    }, "expected { foo: 1, bar: 2 } to not have keys 'foo', or 'baz'");
+
+    // repeat previous tests with Object as arg.
+    err(function(){
+      assert.hasAllKeys({ foo: 1 }, { 'bar': 1 });
+    }, "expected { foo: 1 } to have key 'bar'");
+
+    err(function(){
+      assert.hasAllKeys({ foo: 1 }, { 'bar': 1, 'baz': 1});
+    }, "expected { foo: 1 } to have keys 'bar', and 'baz'");
+
+    err(function(){
+      assert.hasAllKeys({ foo: 1 }, { 'foo': 1, 'bar': 1, 'baz': 1});
+    }, "expected { foo: 1 } to have keys 'foo', 'bar', and 'baz'");
+
+    err(function(){
+      assert.doesNotHaveAllKeys({ foo: 1 }, { 'foo': 1 });
+    }, "expected { foo: 1 } to not have key 'foo'");
+
+    err(function(){
+      assert.doesNotHaveAllKeys({ foo: 1 }, { 'foo': 1 });
+    }, "expected { foo: 1 } to not have key 'foo'");
+
+    err(function(){
+      assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, { 'foo': 1, 'bar': 1});
+    }, "expected { foo: 1, bar: 2 } to not have keys 'foo', and 'bar'");
+
+
+    err(function() {
+      assert.hasAnyKeys({ foo: 1 }, 'baz');
+    }, "expected { foo: 1 } to have key 'baz'");
+
+    err(function(){
+      assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, { 'foo': 1, 'bar': 1});
+    }, "expected { foo: 1, bar: 2 } to not have keys 'foo', and 'bar'");
+
+    err(function(){
+      assert.doesNotHaveAnyKeys({ foo: 1, bar: 2 }, { 'foo': 1, 'baz': 1});
+    }, "expected { foo: 1, bar: 2 } to not have keys 'foo', or 'baz'");
+
+  });
+
   it('lengthOf', function() {
     assert.lengthOf([1,2,3], 3);
     assert.lengthOf('foobar', 6);
diff --git a/test/expect.js b/test/expect.js
index 3daedbb..3d0be05 100644
--- a/test/expect.js
+++ b/test/expect.js
@@ -721,6 +721,86 @@ describe('expect', function () {
     expect({ foo: 1, bar: 2 }).not.have.all.keys({ 'baz': 8, 'foo': 7 });
     expect({ foo: 1, bar: 2 }).not.contain.all.keys({ 'baz': 8, 'foo': 7 });
 
+    if (typeof Map !== 'undefined') {
+      var aKey = {thisIs: 'anExampleObject'};
+      var anotherKey = {doingThisBecauseOf: 'referential equality'};
+
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.have.any.keys(aKey);
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.have.any.keys('thisDoesNotExist', 'thisToo', aKey);
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.have.all.keys(aKey, anotherKey);
+
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.contain.all.keys(aKey);
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.not.contain.all.keys(aKey, 'thisDoesNotExist');
+
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.not.have.any.keys({iDoNot: 'exist'});
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.not.have.any.keys('thisIsNotAkey', {iDoNot: 'exist'}, {33: 20});
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.not.have.all.keys('thisDoesNotExist', 'thisToo', anotherKey);
+
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.have.any.keys([aKey]);
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.have.any.keys([20, 1, aKey]);
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.have.all.keys([aKey, anotherKey]);
+
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.not.have.any.keys([{13: 37}, 'thisDoesNotExist', 'thisToo']);
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.not.have.any.keys([20, 1, {13: 37}]);
+      expect(new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']])).to.not.have.all.keys([aKey, {'iDoNot': 'exist'}]);
+
+      err(function(){
+        expect(new Map().set({ foo: 1 })).to.have.keys();
+      }, "keys required");
+
+      err(function(){
+        expect(new Map().set({ foo: 1 })).to.have.keys([]);
+      }, "keys required");
+
+      err(function(){
+        expect(new Map().set({ foo: 1 })).to.contain.keys();
+      }, "keys required");
+
+      err(function(){
+        expect(new Map().set({ foo: 1 })).to.contain.keys([]);
+      }, "keys required");
+    }
+
+    if (typeof Set !== 'undefined') {
+      var aKey = {thisIs: 'anExampleObject'};
+      var anotherKey = {doingThisBecauseOf: 'referential equality'};
+
+      expect(new Set([aKey, anotherKey])).to.have.any.keys(aKey);
+      expect(new Set([aKey, anotherKey])).to.have.any.keys('thisDoesNotExist', 'thisToo', aKey);
+      expect(new Set([aKey, anotherKey])).to.have.all.keys(aKey, anotherKey);
+
+      expect(new Set([aKey, anotherKey])).to.contain.all.keys(aKey);
+      expect(new Set([aKey, anotherKey])).to.not.contain.all.keys(aKey, 'thisDoesNotExist');
+
+      expect(new Set([aKey, anotherKey])).to.not.have.any.keys({iDoNot: 'exist'});
+      expect(new Set([aKey, anotherKey])).to.not.have.any.keys('thisIsNotAkey', {iDoNot: 'exist'}, {33: 20});
+      expect(new Set([aKey, anotherKey])).to.not.have.all.keys('thisDoesNotExist', 'thisToo', anotherKey);
+
+      expect(new Set([aKey, anotherKey])).to.have.any.keys([aKey]);
+      expect(new Set([aKey, anotherKey])).to.have.any.keys([20, 1, aKey]);
+      expect(new Set([aKey, anotherKey])).to.have.all.keys([aKey, anotherKey]);
+
+      expect(new Set([aKey, anotherKey])).to.not.have.any.keys([{13: 37}, 'thisDoesNotExist', 'thisToo']);
+      expect(new Set([aKey, anotherKey])).to.not.have.any.keys([20, 1, {13: 37}]);
+      expect(new Set([aKey, anotherKey])).to.not.have.all.keys([aKey, {'iDoNot': 'exist'}]);
+
+      err(function(){
+        expect(new Set().add({ foo: 1 })).to.have.keys();
+      }, "keys required");
+
+      err(function(){
+        expect(new Set().add({ foo: 1 })).to.have.keys([]);
+      }, "keys required");
+
+      err(function(){
+        expect(new Set().add({ foo: 1 })).to.contain.keys();
+      }, "keys required");
+
+      err(function(){
+        expect(new Set().add({ foo: 1 })).to.contain.keys([]);
+      }, "keys required");
+    }
+
     err(function(){
       expect({ foo: 1 }).to.have.keys();
     }, "keys required");
@@ -737,7 +817,7 @@ describe('expect', function () {
       expect({ foo: 1 }).to.contain.keys([]);
     }, "keys required");
 
-    var mixedArgsMsg = 'keys must be given single argument of Array|Object|String, or multiple String arguments'
+    var mixedArgsMsg = 'when testing keys against an object or an array you must give a single Array|Object|String argument or multiple String arguments'
 
     err(function(){
       expect({}).contain.keys(['a'], "b");
diff --git a/test/should.js b/test/should.js
index 0d272ba..7f2eb84 100644
--- a/test/should.js
+++ b/test/should.js
@@ -556,12 +556,92 @@ describe('should', function() {
     ({ foo: 1, bar: 2 }).should.not.have.all.keys(['baz', 'foo']);
     ({ foo: 1, bar: 2 }).should.not.contain.all.keys(['baz', 'foo']);
     ({ foo: 1, bar: 2 }).should.not.have.all.keys({ 'baz': 8, 'foo': 7 });
+
     ({ foo: 1, bar: 2 }).should.not.contain.all.keys({ 'baz': 8, 'foo': 7 });
 
     ({ 1: 1, 2: 2 }).should.have.keys(1, 2);
     ({ 1: 1, 2: 2 }).should.have.any.keys(1, 3);
     ({ 1: 1, 2: 2 }).should.contain.keys(1);
 
+    if (typeof Map !== 'undefined') {
+      var aKey = {thisIs: 'anExampleObject'};
+      var anotherKey = {doingThisBecauseOf: 'referential equality'};
+
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.have.any.keys(aKey);
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.have.any.keys('thisDoesNotExist', 'thisToo', aKey);
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.have.all.keys(aKey, anotherKey);
+
+
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.contain.all.keys(aKey);
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.not.contain.all.keys(aKey, 'thisDoesNotExist');
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.not.have.any.keys({iDoNot: 'exist'});
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.not.have.all.keys('thisDoesNotExist', 'thisToo', anotherKey);
+
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.have.any.keys([aKey]);
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.have.any.keys([20, 1, aKey]);
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.have.all.keys([aKey, anotherKey]);
+
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.not.have.any.keys([{13: 37}, 'thisDoesNotExist', 'thisToo']);
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.not.have.any.keys([20, 1, {13: 37}]);
+      new Map([[aKey, 'aValue'], [anotherKey, 'anotherValue']]).should.not.have.all.keys([aKey, {'iDoNot': 'exist'}]);
+
+      err(function(){
+        new Map().set({ foo: 1 }).should.have.keys();
+      }, "keys required");
+
+      err(function(){
+        new Map().set({ foo: 1 }).should.have.keys([]);
+      }, "keys required");
+
+      err(function(){
+        new Map().set({ foo: 1 }).should.contain.keys();
+      }, "keys required");
+
+      err(function(){
+        new Map().set({ foo: 1 }).should.contain.keys([]);
+      }, "keys required");
+    }
+
+    if (typeof Set !== 'undefined') {
+      var aKey = {thisIs: 'anExampleObject'};
+      var anotherKey = {doingThisBecauseOf: 'referential equality'};
+
+      new Set([aKey, anotherKey]).should.have.any.keys(aKey);
+      new Set([aKey, anotherKey]).should.have.any.keys('thisDoesNotExist', 'thisToo', aKey);
+      new Set([aKey, anotherKey]).should.have.all.keys(aKey, anotherKey);
+
+      new Set([aKey, anotherKey]).should.contain.all.keys(aKey);
+      new Set([aKey, anotherKey]).should.not.contain.all.keys(aKey, 'thisDoesNotExist');
+
+      new Set([aKey, anotherKey]).should.not.have.any.keys({iDoNot: 'exist'});
+      new Set([aKey, anotherKey]).should.not.have.any.keys('thisIsNotAkey', {iDoNot: 'exist'}, {33: 20});
+      new Set([aKey, anotherKey]).should.not.have.all.keys('thisDoesNotExist', 'thisToo', anotherKey);
+
+      new Set([aKey, anotherKey]).should.have.any.keys([aKey]);
+      new Set([aKey, anotherKey]).should.have.any.keys([20, 1, aKey]);
+      new Set([aKey, anotherKey]).should.have.all.keys([aKey, anotherKey]);
+
+      new Set([aKey, anotherKey]).should.not.have.any.keys([{13: 37}, 'thisDoesNotExist', 'thisToo']);
+      new Set([aKey, anotherKey]).should.not.have.any.keys([20, 1, {13: 37}]);
+      new Set([aKey, anotherKey]).should.not.have.all.keys([aKey, {'iDoNot': 'exist'}]);
+
+      err(function(){
+        new Set().add({ foo: 1 }).should.have.keys();
+      }, "keys required");
+
+      err(function(){
+        new Set().add({ foo: 1 }).should.have.keys([]);
+      }, "keys required");
+
+      err(function(){
+        new Set().add({ foo: 1 }).should.contain.keys();
+      }, "keys required");
+
+      err(function(){
+        new Set().add({ foo: 1 }).should.contain.keys([]);
+      }, "keys required");
+    }
+
     err(function(){
       ({ foo: 1 }).should.have.keys();
     }, "keys required");
@@ -578,7 +658,7 @@ describe('should', function() {
       ({ foo: 1 }).should.contain.keys([]);
     }, "keys required");
 
-    var mixedArgsMsg = 'keys must be given single argument of Array|Object|String, or multiple String arguments'
+    var mixedArgsMsg = 'when testing keys against an object or an array you must give a single Array|Object|String argument or multiple String arguments'
 
     err(function(){
       ({}).should.contain.keys(['a'], "b");