How do we assert for equality of ES6 Maps and Sets?
For example:
// ES6 Map
var m1 = new Map();
m1.set('one', 1);
var m2 = new Map();
m2.set('two', 2);
assert.deepEqual(m1,m2); // outputs: passed.
// ES6 Set
var s1 = new Set();
s1.add(1);
var s2 = new Set();
s2.add(2);
assert.deepEqual(s1,s2); // outputs: passed.
The intention is to assert that the elements of the Sets/Maps are equal. Both the both assertions should fail.
Is there an equivalent of deepEqual
for Sets/Maps? In other words, short of manually iterating the elements, how do we test for Set/Map equality deeply?
If there is no way in QUnit, is there a unit testing tool that works for ES6 Sets and Maps?
Edit
In Firefox, which supports Array.from()
, I have been comparing sets and maps by:
assert.deepEqual(Array.from(m1), Array.from(m2));
But this does not work with other browsers, which do not support Array.from()
. Even with an Array.from
polyfill, Chrome / IE does not work - Array.from(set)
always produces an empty array regardless of the set contents. This is possibly due to these browsers' lack of support for generic iterables.
Secondly, reducing it into a comparison of Arrays may not be always appropriate. We would end up with what I consider to be false positives:
var s = new Set();
s.add([1,2]);
var m = new Map();
m.set(1,2);
assert.deepEqual(Array.from(s), Array.from(m)); // outputs: passed.
Update:
A patch is currently in the works at QUnit to extend deepEqual()
to handle ES6 Sets and Maps. When that pull request gets merged in, we should be able to use deepEqual()
to compare Sets and Maps. (-:
The qunit-fixture element is a container for some HTML that your tests can assert against. After each test, QUnit will reset it back to what it was before the test started, so that the next test can run without having to worry about what the previous test added or removed.
We would follow the conventions: Place Source JS/TS files in src folder and tests typescript files in tests folder. Basically, it is installation of npm packages for TypeScript, Test framework (e.g. Jasmine/Mocha/Jest) and specifying test script required to execute test cases as explained further.
Run tests in Visual StudioYou can run the tests by clicking the Run All link in Test Explorer. Or, you can run tests by selecting one or more tests or groups, right-clicking, and selecting Run from the shortcut menu. Tests run in the background, and Test Explorer automatically updates and shows the results.
Exhaustive Map comparison using higher-order functions
I'm going to approach this the same way I approached array comparison in this similar answer: How to compare arrays in JavaScript?
I'm going to go thru the code bit-by-bit, but I'll have a complete runnable example at the end
Shallow comparison
First off, we're going to start with a generic Map comparison function. This way we can do all sorts of comparisons on Map objects, not just testing for equality
This mapCompare
function agrees with our intuition about how Maps should be compared - we compare each key from m1
against each key from m2
. Note, this specific comparator is doing shallow comparison. We'll handle deep comparison in just a moment
const mapCompare = f => (m1, m2) => {
const aux = (it, m2) => {
let {value, done} = it.next()
if (done) return true
let [k, v] = value
return f (v, m2.get(k), $=> aux(it, m2))
}
return aux(m1.entries(), m2) && aux(m2.entries(), m1)
}
The only thing that might not be immediately clear is the $=> aux(it, m2)
thunk. Maps have a built-in generator, .entries()
, and we can take advantage of the lazy evaluation by returning an early false
answer as soon as non-matching key/value pair is found. That means we have to write our comparators in a slightly special way.
const shortCircuitEqualComparator = (a, b, k) =>
a === b ? true && k() : false
a
and b
are values of m1.get(somekey)
and m2.get(somekey)
respectively. iff the two values are strictly equal (===
), only then do we want to continue the comparison – in this case we return true && k()
where k()
is the remainder of the key/value pair comparison. On the other hand, if a
and b
do not match, we can return an early false
and skip comparing the rest of the values – ie, we already know that m1
and m2
do not match if any a
/b
pair do not match.
Finally, we can define mapEqual
- it's simple too. It's just mapCompare
using our special shortCircuitEqualComparator
const mapEqual = mapCompare (shortCircuitEqualComparator)
Let's take a quick look at how this works
// define two maps that are equal but have keys in different order
const a = new Map([['b', 2], ['a', 1]])
const b = new Map([['a', 1], ['b', 2]])
// define a third map that is not equal
const c = new Map([['a', 3], ['b', 2]])
// verify results
// a === b should be true
// b === a should be true
console.log('true', mapEqual(a, b)) // true true
console.log('true', mapEqual(b, a)) // true true
// a === c should be false
// c === a should be false too
console.log('false', mapEqual(a, c)) // false false
console.log('false', mapEqual(c, a)) // false false
Heck yes. Things are looking good ...
Deep comparisons with Rick & Morty
Now that we have a fricken sweet mapCompare
generic, deep comparison is a breeze. Take notice that we're actually implementing mapDeepCompare
using mapCompare
itself.
We use a custom comparator that simply checks if a
and b
are both Map objects – if so, we recurse using mapDeepCompare
on the nested Maps; also being mindful to call ... && k()
to ensure the remaining key/value pairs are compared. If, a
or b
is a non-Map object, normal comparison is doing using f
and we pass the continuation k
along directly
const mapDeepCompare = f => mapCompare ((a, b, k) => {
console.log(a, b)
if (a instanceof Map && b instanceof Map)
return mapDeepCompare (f) (a,b) ? true && k() : false
else
return f(a,b,k)
})
Now with mapDeepCompare
, we can perform any type of deep comparison on nested Maps. Remember, equality is just one of the things we can be checking.
Without further ado, mapDeepEqual
. Of importance, we get to reuse our shortCircuitEqualComparator
that we defined before. This very clearly demonstrates that our comparators can be (re)used for shallow or deep Map comparisons.
const mapDeepEqual = mapDeepCompare (shortCircuitEqualComparator)
Let's see it work
// define two nested maps that are equal but have different key order
const e = new Map([
['a', 1],
['b', new Map([['c', 2]])]
])
const f = new Map([
['b', new Map([['c', 2]])],
['a', 1]
])
// define a third nested map that is not equal
const g = new Map([
['b', new Map([
['c', 3]
])],
['a', 1]
])
// e === f should be true
// f === e should also be true
console.log('true', mapDeepEqual(e, f)) // true
console.log('true', mapDeepEqual(f, e)) // true
// e === g should be false
// g === e should also be false
console.log('false', mapDeepEqual(e, g)) // false
console.log('false', mapDeepEqual(g, e)) // false
OK, and just to make sure. What happens if we call mapEqual
on our nested Maps e
and f
? Since mapEqual
does shallow comparison, we expect that the result should be false
console.log('false', mapEqual(e, f)) // false
console.log('false', mapEqual(f, e)) // false
And there you have it. Shallow and deep comparison of ES6 Map objects. A nearly identical set of functions could be written to support ES6 Set. I'll leave this as an exercise for the readers.
Runnable code demo
This is all of the code above compiled into a single runnable demo. Each console.log
call outputs <expected>, <actual>
. So true, true
or false, false
would be a passing test – whereas true, false
would be a failed test.
// mapCompare :: ((a, a, (* -> Bool)) -> Bool) -> (Map(k:a), Map(k:a)) -> Bool
const mapCompare = f => (m1, m2) => {
const aux = (it, m2) => {
let {value, done} = it.next()
if (done) return true
let [k, v] = value
return f (v, m2.get(k), $=> aux(it, m2))
}
return aux(m1.entries(), m2) && aux(m2.entries(), m1)
}
// mapDeepCompare :: ((a, a, (* -> Bool)) -> Bool) -> (Map(k:a), Map(k:a)) -> Bool
const mapDeepCompare = f => mapCompare ((a, b, k) => {
if (a instanceof Map && b instanceof Map)
return mapDeepCompare (f) (a,b) ? true && k() : false
else
return f(a,b,k)
})
// shortCircuitEqualComparator :: (a, a, (* -> Bool)) -> Bool
const shortCircuitEqualComparator = (a, b, k) =>
a === b ? true && k() : false
// mapEqual :: (Map(k:a), Map(k:a)) -> Bool
const mapEqual = mapCompare (shortCircuitEqualComparator)
// mapDeepEqual :: (Map(k:a), Map(k:a)) -> Bool
const mapDeepEqual = mapDeepCompare (shortCircuitEqualComparator)
// fixtures
const a = new Map([['b', 2], ['a', 1]])
const b = new Map([['a', 1], ['b', 2]])
const c = new Map([['a', 3], ['b', 2]])
const d = new Map([['a', 1], ['c', 2]])
const e = new Map([['a', 1], ['b', new Map([['c', 2]])]])
const f = new Map([['b', new Map([['c', 2]])], ['a', 1]])
const g = new Map([['b', new Map([['c', 3]])], ['a', 1]])
// shallow comparison of two equal maps
console.log('true', mapEqual(a, b))
console.log('true', mapEqual(b, a))
// shallow comparison of two non-equal maps (differing values)
console.log('false', mapEqual(a, c))
console.log('false', mapEqual(c, a))
// shallow comparison of two other non-equal maps (differing keys)
console.log('false', mapEqual(a, d))
console.log('false', mapEqual(d, a))
// deep comparison of two equal nested maps
console.log('true', mapDeepEqual(e, f))
console.log('true', mapDeepEqual(f, e))
// deep comparison of two non-equal nested maps
console.log('false', mapDeepEqual(e, g))
console.log('false', mapDeepEqual(g, e))
// shallow comparison of two equal nested maps
console.log('false', mapEqual(e, f))
console.log('false', mapEqual(f, e))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With