soft-assert
Version:
Soft Asserts JSON, Array and other data
455 lines (411 loc) • 17.3 kB
JavaScript
const assert = require("chai").assert;
const _ = require("lodash");
const constant = require("../common/constant");
/**
* Asserts json, array, array of json and other primitive types
* Provides both full deep assertion and contains deep assertion
* Ability to support Soft Assertion is also added
*
* @class Assertion
* @author sarunya.d
*/
class AssertionHelper {
constructor() {
this.softAssertJson = null;
this.softAssertCount = 0;
}
/**
* Sets key names to be ignored during assertion
*
* @param {array} ignoreKeys keys to be ignored while asserting
*/
setIgnoreKeys(ignoreKeys) {
const me = this;
me.ignoreKeys = ignoreKeys || [];
me.ignoreKeys = _.isArray(ignoreKeys) ? ignoreKeys : [ignoreKeys];
}
//check if given json is an array
_isJSON(x) {
try {
return (_.isObjectLike(x) && JSON.parse(JSON.stringify(x)) && !_.isArray(x));
} catch (e) {
return false;
}
}
_removeFromArray(array, index) {
array.splice(index, 1);
}
_isIgnoreKey(key) {
const me = this;
return _.indexOf(me.ignoreKeys, key) >= 0
}
//Asserts if actualArr deeply equals expectedArr
_assertArray(actualArr, expectedArr, key, isType) {
const me = this;
if (me._isIgnoreKey(key)) {
return true;
} else if(isType && me._isDataType(actualArr, expectedArr)) {
return true;
}
//prevents deletion from actual data array
actualArr = _.cloneDeep(actualArr);
expectedArr = _.cloneDeep(expectedArr);
let expectedMatchingIndexes = [];
if (expectedArr.length > 0 && me._isJSON(expectedArr[0])) {
let actualLen = actualArr.length;
let expectLen = expectedArr.length;
//loops for each data and checks with expected data is present in actual data array
for (let i = 0; i < expectLen; i++) {
for (let j = 0; j < actualLen; j++) {
try {
if (me._assert(actualArr[j], expectedArr[i], key + "." + j, false, isType)) {
me._removeFromArray(actualArr, j);
expectedMatchingIndexes.push(i);
--actualLen;
break;
}
} catch (error) {
continue;
}
}
}
}
//remove matching data from expectedArr, to filter the data not present in actualArr
expectedMatchingIndexes = _.reverse(_.sortBy(expectedMatchingIndexes));
_.each(expectedMatchingIndexes, (index) => {
me._removeFromArray(expectedArr, index);
})
if (!me.jsonContains) {
//Full JSON Assertion
return me._chaiSameDeepMembers(actualArr, expectedArr, key);
} else {
//Contains assertion
return me._chaiIncludeDeepMembers(actualArr, expectedArr, key);
}
}
_assertString(actualStr, expectedStr, key="$", ignoreDiff, isType) {
const me = this;
if(me.jsonContains) {
ignoreDiff = me._checkRegex(actualStr, expectedStr);
ignoreDiff = ignoreDiff || actualStr.includes(expectedStr);
}
return me._chaiDeepEqual(actualStr, expectedStr, key, ignoreDiff, isType);
}
_checkRegex(str, regex) {
var isValid = true;
try {
var patt = new RegExp(regex);
return patt.test(str)
} catch(e) {
isValid = false;
}
}
//Deep asserts actual with expected. ignoreDiff true is passed, to ignore assertion for given keys
_assert(actual, expected, key, ignoreDiff = false, isType = false) {
const me = this;
key = key || constant.rootKey;
if (me._isIgnoreKey(key)) {
return true;
}
let actualKeys = _.keys(actual);
let expectedKeys = _.keys(expected);
let missingKeysInActual = _.clone(actualKeys);
let actualIgnoreKeys = _.intersection(me.ignoreKeys, actualKeys);
let expectedIgnoreKeys = _.intersection(me.ignoreKeys, expectedKeys);
//Delete ignored common keys from actual data
_.each(actualIgnoreKeys, function (actIgnoreKey) {
delete actual[actIgnoreKey];
})
//Delete ignored common keys from expected data
_.each(expectedIgnoreKeys, function (expIgnoreKey) {
delete expected[expIgnoreKey];
})
if (_.isArray(expected)) {
return me._assertArray(actual, expected, key, isType);
} else if (me._isJSON(expected)) {
let assertion = true;
//If the data is JSON, iterate for each key present in expected data and assert recursively
_.each(expectedKeys, (expectKey) => {
if (typeof actual[expectKey] == typeof expected[expectKey]) {
if (me._isJSON(actual[expectKey])) {
assertion = me._assert(actual[expectKey], expected[expectKey], key + "." + expectKey, ignoreDiff, isType) && assertion;
} else if (_.isArray(actual[expectKey])) {
assertion = me._assertArray(actual[expectKey], expected[expectKey], key + "." + expectKey, isType) && assertion;
} else if(_.isString(actual[expectKey])) {
return me._assertString(actual[expectKey], expected[expectKey], key + "." + expectKey, ignoreDiff, isType) && assertion;
} else {
assertion = me._chaiDeepEqual(actual[expectKey], expected[expectKey], key + "." + expectKey, ignoreDiff, isType) && assertion;
}
} else {
assertion = me._chaiDeepEqual(actual[expectKey], expected[expectKey], key + "." + expectKey, ignoreDiff, isType) && assertion;
}
//remove the key from array to get the list disjoint keys in actual and expected data
me._removeFromArray(missingKeysInActual, missingKeysInActual.indexOf(expectKey));
})
//Assert the missing keys in actual keys only for Full JSON Assertion
if (!me.jsonContains) {
_.each(missingKeysInActual, function (missingKey) {
assertion = me._chaiDeepEqual(actual[missingKey], expected[missingKey], key + "." + missingKey, ignoreDiff, isType) && assertion;
})
}
return assertion;
} else if(_.isString(expected)) {
return me._assertString(actual, expected, key, ignoreDiff, isType);
} else {
return me._chaiDeepEqual(actual, expected, key, ignoreDiff, isType);
}
}
_constructJsonFromKeyValue(json, fullKey, value) {
if (!_.isUndefined(value)) {
json = _.set(json, fullKey, value);
}
}
_constructJsonDiffAssertionError(msg) {
const me = this;
let compiledError = null;
let firstMsg = "";
let index = 1;
let errorMsg = "";
if (me.jsonDiffArray && me.jsonDiffArray.length > 0) {
_.forEach(me.jsonDiffArray, function (jsonDiff) {
let error = _.cloneDeep(jsonDiff.error);
if (!compiledError) {
compiledError = jsonDiff.error;
firstMsg = compiledError.message;
compiledError.actual = {
"$": {}
};
compiledError.expected = {
"$": {}
};
compiledError.message = "";
errorMsg = "\n";
}
me._constructJsonFromKeyValue(compiledError.actual, jsonDiff.key, error.actual);
me._constructJsonFromKeyValue(compiledError.expected, jsonDiff.key, error.expected);
if (index < 5) {
errorMsg += `\t${index} : key -> ${jsonDiff.key} \t\t message: ${error.message}\n`;
} else if (index == 5) {
errorMsg += "..." + "\n";
}
++index;
})
let message = "";
compiledError.actual = compiledError.actual[constant.rootKey];
compiledError.expected = compiledError.expected[constant.rootKey];
if (me._isJSON(compiledError.actual)) {
message = me._constructAssertionMessage(true) + _.sortBy(_.keys(compiledError.actual)).join("\n\t");
} else if (_.isArray(compiledError.actual)) {
message = me._constructAssertionMessage(false);
}
msg = (_.isEmpty(msg)) ? "" : `\n\n\tCustom Message : ${msg}\n`;
compiledError.message = message + msg + errorMsg;
compiledError.stack = me._splitStackTrace(compiledError.stack, constant.deepAssertionMethods);
return compiledError;
}
}
_constructAssertionMessage(isJSON) {
const me = this;
let message = "\n" + (me.jsonContains) ? constant.containsAssertionMsg : constant.deepAssertionMsg;
if (isJSON) {
return message + constant.jsonMsg;
}
return message + constant.arrayMsg;
}
/**
* Throw the error after concatenating the differences (incase of json)
* If error is undefined, assertion has passed.
* Therefore, error should not be passed, which will stop the assertion at that point
*/
_throwJsonDiffAssertionError(msg) {
const me = this;
let error = me._constructJsonDiffAssertionError(msg);
if (error) {
throw error;
}
}
_constructSoftAssertJson(msg) {
const me = this;
let error = me._constructJsonDiffAssertionError(msg);
if (!error) {
++me.softAssertCount;
return;
}
let stack = _.cloneDeep(error.stack);
if (!me.softAssertJson) {
me.softAssertJson = _.cloneDeep(error);
me.softAssertJson.message = "";
me.softAssertJson.actual = {};
me.softAssertJson.expected = {};
me.softAssertJson.stackMessage = "";
}
stack = me._splitStackTrace(stack, constant.softAssertionMethods)
me.softAssertJson.message += `\n\n${constant.softAsssertionMsg} ` + (++me.softAssertCount) + ":" + "\n" + error.message + "Stack: " + stack;
me.softAssertJson.actual[constant.softAsssertionMsg + me.softAssertCount] = error.actual;
me.softAssertJson.expected[constant.softAsssertionMsg + me.softAssertCount] = error.expected;
}
_splitStackTrace(stack, assertionArray) {
let stackArray = stack.split(constant.stackStartsWith + assertionArray[0]);
if (stackArray && stackArray.length <= 1) {
stackArray = stack.split(constant.stackStartsWith + assertionArray[1]);
} else {
let stack = stackArray[0].split(constant.stackStartsWith) + _.last(stackArray);
return stack;
}
return _.last(stackArray)
}
_softThrowJsonDiffArray() {
const me = this;
if (me.softAssertJson) {
let error = _.cloneDeep(me.softAssertJson);
me.softAssertJson = null;
me.softAssertCount = 0;
throw error;
}
}
_addToJsonDiffArray(actual, expected, key, error) {
const me = this;
me.jsonDiffArray.push({
key: key,
error: error
})
}
_realType(value) {
const me = this;
if (value == null) return "null";
if (Array.isArray(value)) return "array";
if (me._isJSON(value)) return "json";
return (typeof value);
}
_isDataType(value, expected) {
const me = this;
let typeValue = me._realType(value);
let typeSchema = me._realType(expected);
if (typeSchema != "string") {
return (false);
}
let angleBracketsRegEx = /^<(.*)>$/
let match = angleBracketsRegEx.exec(expected);
if (match == null) {
return (false);
}
let expectedType = match[1];
if (typeValue == "null") {
return (true);
}
if (expectedType == "array") {
return (Array.isArray(value));
} else {
return (typeValue == expectedType);
}
}
_chaiDeepEqual(actual, expected, key, ignoreDiff = false, isType = false) {
const me = this;
try {
if(isType && me._isDataType(actual, expected)) {
return true;
}
if (actual && me._isJSON(actual))
actual = JSON.parse(JSON.stringify(actual));
if (expected && me._isJSON(expected))
expected = JSON.parse(JSON.stringify(expected));
//check if same type
assert.deepEqual(actual, expected);
} catch (error) {
if (!ignoreDiff && !me._isIgnoreKey(key)) {
me._addToJsonDiffArray(actual, expected, key, error);
return false;
}
}
return true;
}
_chaiSameDeepMembers(actual, expected, key) {
const me = this;
try {
assert.sameDeepMembers(actual, expected);
} catch (error) {
if (!me._isIgnoreKey(key)) {
me._addToJsonDiffArray(actual, expected, key, error);
return false;
}
}
return true;
}
_chaiIncludeDeepMembers(actual, expected, key) {
const me = this;
try {
assert.includeDeepMembers(actual, expected);
} catch (error) {
if (!me._isIgnoreKey(key)) {
me._addToJsonDiffArray(actual, expected, key, error);
return false;
}
}
return true;
}
_deleteIgnoredKeys(actual, expected) {
const me = this;
let ignoreAllWithName = [];
_.each(me.ignoreKeys, function (ignoreKey) {
if (_.startsWith(ignoreKey, `${[constant.rootKey]}.`)) {
let parentKeys = _.split(ignoreKey);
let childKey = _.last(parentKeys);
parentKeys = _.slice(parentKeys, 1, parentKeys.length - 1);
parentKeys = _.join(parentKeys, ".");
let actualParent = _.get(actual, parentKeys, null);
let expectedParent = _.get(expected, parentKeys, null);
if (actualParent)
delete actualParent[childKey];
if (expectedParent)
delete expectedParent[childKey];
}
})
}
_prepareAndAssert(actual, expected, ignoreKeys, jsonContains = false, isType = false) {
const me = this;
me.jsonDiffArray = [];
me.jsonContains = jsonContains;
expected = _.cloneDeep(expected);
actual = _.cloneDeep(actual);
me.setIgnoreKeys(ignoreKeys);
me._deleteIgnoredKeys(actual, expected);
me._assert(actual, expected, constant.rootKey, null, isType);
}
_prepareAssertJsonArrayByKeys(actualArr, expectedArr, uniqueKeys) {
const me = this;
uniqueKeys = (_.isArray(uniqueKeys)) ? uniqueKeys : [uniqueKeys];
let comparisonActArr = [];
let notFoundActArr = [];
let comparisonExpArr = [];
_.each(actualArr, function (actual) {
let expectedIndex = -1;
let index = 0;
_.each(expectedArr, function (expected) {
let matchesAll = true;
//check all the uniquekeys matches
for (let i = 0; i < uniqueKeys.length; i++) {
if (_.get(actual, uniqueKeys[i]) != _.get(expected, uniqueKeys[i])) {
matchesAll = false;
break;
}
}
if (matchesAll) {
expectedIndex = index;
return;
}
++index;
})
if (expectedIndex === -1) {
notFoundActArr.push(actual);
} else {
comparisonActArr.push(actual);
comparisonExpArr.push(expectedArr[expectedIndex]);
me._removeFromArray(expectedArr, expectedIndex);
}
})
comparisonActArr = _.union(comparisonActArr, notFoundActArr);
comparisonExpArr = _.union(comparisonExpArr, expectedArr);
return [comparisonActArr, comparisonExpArr];
}
}
module.exports = AssertionHelper;