Hi
I am trying to set up unit tests for my project and I'm wondering about comparing objects...
This works...
This works...
Hi
I am trying to set up unit tests for my project and I'm wondering about comparing objects...
This works...
This works...
EDIT: To respond to your edited question:
For your first example, both equals() and "==" work because you are comparing two references to a single dictionary instance.
In you second example, each variable is a reference to a distinct dictionary instance (with identical contents):
- "==" compares by reference, so it won't work to compare the contents of two dictionary instances
- It's not 100% clear to me what Object.equals() does for dictionaries (may or may not be equivalent to "==" in this case), but it apparently doesn't do the deep value comparison that you would want it to do.
Neither of those works to compare dictionaries, but you can write your own function.
function dictionaryEquals(a, b) { if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } var a_keys = a.keys(); if (a_keys.size() != b.keys().size()) { return false; } for (var i = 0; i < a_keys.size(); i++) { if (!b.hasKey(a_keys[i])) { return false; } if (b.get(a_keys[i]) != a.get(a_keys[i])) { return false; } } return true; }
Quick update and it's good news. After your helpful example above, I started writing a deepEqual function then came across this post from a couple of years ago that provides recursive verifyDictEquals(), verifyArrayEquals() and verifyValueEquals() functions.
forums.garmin.com/.../storage-querk
Below is a class that allows a unit test to be expressed as a riteway unit test:
(:test)
class ritewayTest
{
function assert(logger, theTest)
{
logger.debug("Given: " + theTest["Given"]);
logger.debug("Should: " + theTest["Should"]);
logger.debug("Actual: " + theTest["Actual"]);
logger.debug("Expected:" + theTest["Expected"]);
return verifyValueEquals(logger, theTest["Actual"], theTest["Expected"]);
}
// deep compare dicts `a` and `b`, returns true if equals
function verifyDictEquals(logger, a, b)
{
var a_sz = a.size();
var b_sz = b.size();
if (a_sz != b_sz) {
return false;
}
var keys = a.keys();
for (var i = 0; i < a_sz; ++i) {
var key = keys[i];
if (!verifyValueEquals(logger, a[key], b[key])) {
return false;
}
}
return true;
}
// deep compare arrays `a` and `b`, returns true if equals
function verifyArrayEquals(logger, a, b)
{
var a_sz = a.size();
var b_sz = b.size();
if (a_sz != b_sz) {
return false;
}
for (var i = 0; i < a_sz; ++i) {
logger.debug(i.toString());
if (!verifyValueEquals(logger, a[i], b[i])) {
return false;
}
}
return true;
}
// deep compare `a` with `b`, returns true if all of these hold true
//
// 1) value types are known
// 2) value types are the same
// 3) values are equal
//
function verifyValueEquals(logger, a, b)
{
logger.debug(Lang.format("Expected '$1$', got '$2$'.", [ a, b ]));
if ((a instanceof Lang.Dictionary) && (b instanceof Lang.Dictionary))
{
return verifyDictEquals(logger, a, b);
} else if ((a instanceof Lang.Array) && (b instanceof Lang.Array))
{
return verifyArrayEquals(logger, a, b);
} else if ((a instanceof Lang.String) && (b instanceof Lang.String))
{
return a.equals(b);
} else if ((a instanceof Lang.Double) && (b instanceof Lang.Double))
{
return Math.abs(b - a) < 0.001;
} else if ((a instanceof Lang.Float) && (b instanceof Lang.Float))
{
return Math.abs(b - a) < 0.001;
} else if ((a instanceof Lang.Long) && (b instanceof Lang.Long))
{
return a == b;
} else if ((a instanceof Lang.Number) && (b instanceof Lang.Number))
{
return a == b;
} else if ((a instanceof Lang.Boolean) && (b instanceof Lang.Boolean))
{
return a == b;
}
return false;
}
// End of class
}
(:test)
function testArrayOfDictionary(logger) {
var expectedData = [
{"distance"=>5, "title"=>"Distance is 5"},
{"distance"=>3, "title"=>"Distance is 3"},
];
var actualData = [
{"distance"=>5, "title"=>"Distance is 5"},
{"distance"=>3, "title"=>"Distance is 3"},
];
var riteway = new ritewayTest();
return riteway.assert(logger, {
"Given" => "an array of segments from the server",
"Should"=> "sort the array into ascending distance order",
"Actual" => actualData,
"Expected" => expectedData
});
}
Hope this is useful to someone in the future
Gary
Thanks for the code!
One nitpick for the code for comparing dictionaries:
function verifyDictEquals(logger, a, b) { var a_sz = a.size(); var b_sz = b.size(); if (a_sz != b_sz) { return false; } var keys = a.keys(); for (var i = 0; i < a_sz; ++i) { var key = keys[i]; if (!verifyValueEquals(logger, a[key], b[key])) { return false; } } return true; }
I wouldn't skip the check to verify that b.hasKey(key) (as in my example) because otherwise it won't be possible to distinguish the case where a dictionary has key X with value null from the case where dictionary lacks key X.
e.g. you don't want to say that the following dictionaries are equal:
{
"data1"=>123
"data2"=>null
}
{
"data1"=>123
"data3"=>null
}
Interestingly the test fails with or without the b.hasKey(key) addition although I am not entirely sure why it fails without the addition! I agree this check should be there. Updated code below:
(:test)
class ritewayTest
{
function assert(logger, theTest)
{
logger.debug("Given: " + theTest["Given"]);
logger.debug("Should: " + theTest["Should"]);
logger.debug("Actual: " + theTest["Actual"]);
logger.debug("Expected:" + theTest["Expected"]);
return verifyValueEquals(logger, theTest["Expected"], theTest["Actual"]);
}
// deep compare dicts `a` and `b`, returns true if equals
function verifyDictEquals(logger, a, b)
{
var a_sz = a.size();
var b_sz = b.size();
if (a_sz != b_sz) {
return false;
}
var keys = a.keys();
for (var i = 0; i < a_sz; ++i) {
var key = keys[i];
if (!b.hasKey(key)) {
return false;
}
if (!verifyValueEquals(logger, a[key], b[key])) {
return false;
}
}
return true;
}
// deep compare arrays `a` and `b`, returns true if equals
function verifyArrayEquals(logger, a, b)
{
var a_sz = a.size();
var b_sz = b.size();
if (a_sz != b_sz) {
return false;
}
for (var i = 0; i < a_sz; ++i) {
logger.debug("arrayIndex:" + i.toString());
if (!verifyValueEquals(logger, a[i], b[i])) {
return false;
}
}
return true;
}
// forums.garmin.com/.../storage-querk
// deep compare `a` with `b`, returns true if all of these hold true
//
// 1) value types are known
// 2) value types are the same
// 3) values are equal
//
function verifyValueEquals(logger, a, b)
{
logger.debug(Lang.format("Expected '$1$', got '$2$'.", [ a, b ]));
if ((a instanceof Lang.Dictionary) && (b instanceof Lang.Dictionary))
{
return verifyDictEquals(logger, a, b);
} else if ((a instanceof Lang.Array) && (b instanceof Lang.Array))
{
return verifyArrayEquals(logger, a, b);
} else if ((a instanceof Lang.String) && (b instanceof Lang.String))
{
return a.equals(b);
} else if ((a instanceof Lang.Double) && (b instanceof Lang.Double))
{
return Math.abs(b - a) < 0.001;
} else if ((a instanceof Lang.Float) && (b instanceof Lang.Float))
{
return Math.abs(b - a) < 0.001;
} else if ((a instanceof Lang.Long) && (b instanceof Lang.Long))
{
return a == b;
} else if ((a instanceof Lang.Number) && (b instanceof Lang.Number))
{
return a == b;
} else if ((a instanceof Lang.Boolean) && (b instanceof Lang.Boolean))
{
return a == b;
}
return false;
}
// End of class
}
(:test)
function testArrayOfDictionary(logger) {
/*
var expectedData = [
{"distance"=>5, "title"=>"Distance is 5"},
{"distance"=>3, "data2"=>null, "title"=>"Distance is 3"},
];
var actualData = [
{"distance"=>5, "title"=>"Distance is 5"},
{"distance"=>3, "data3"=>null, "title"=>"Distance is 3"},
];
*/
var expectedData = {"data1"=>123, "data2"=>null};
var actualData = {"data1"=>123, "data3"=>null};
var riteway = new ritewayTest();
return riteway.assert(logger, {
"Given" => "an array of segments from the server",
"Should"=> "sort the array into ascending distance order",
"Actual" => actualData,
"Expected" => expectedData
});
}
Yeah the forum is janky and often frustrating.
The reason it fails either way is because you aren't considering null to be equal to null in verifyValueEquals
Instead of return false as the final line I would have:
return a == b
(And you could fold your Number and Boolean cases into that default case.).
(Given that monkey c -- and most languages other than SQL -- consider null to equall null, I think this behavior would be reasonable.)
One thing you may run into with dictionaries is the keys may exist in the two, but might not be in the same order
I think you'll find this is accounted for in the comparison functions posted above.
have you considered just using something simpler like arrays?
I agree with this in general tho. Dictionaries are incredibly expensive (memory-wise) in CIQ's resource-constrained environment, and I almost never use them unless I have to. (i.e. Unless the input or output of a CIQ SDK function demands that I used them.)