JavaScript assertion function that allows sparse attributes and values

Most JavaScript assertion libraries allow you to compare if objects are equal, if they match or if one includes the other. But sometimes you don’t wont to compare in all the objects properties math or if all array items match. Say you want to test for the array of objects

let actual = [{
  type: FETCH_ORDERS,
  status: 'before',
  payload: {},
}, {
  type: FETCH_ORDERS,
  status: 'success',
  payload: {
    entities: {
      items: [{
        key: 1,
        value: 1
      }, {
        key: 2,
        value: 2
      }],
      menus: [{
        id: 'pret',
        name: 'Pret'
      }, {
        id: 'leon',
        name: 'Leon'
      }]
    },
    result: ['pret', 'leon']
  }
}]

when in fact you’re only interested only in

[{
  type: FETCH_ORDERS,
  status: 'before',
  payload: {},
}, {
  type: FETCH_ORDERS,
  status: 'success',
  payload: ...
}]

For example if using mjackson/expect library you can check for that by doing:

let asserted = [{
  type: FETCH_ORDERS,
  status: 'before',
  payload: {},
}, {
  type: FETCH_ORDERS,
  status: 'success'
}]

expect(actual.length).toEqual(2)
expect(actual[0]).toEqual(asserted[0])
// Dont compare the payload
expect(actual[1]).toInclude(asserted[1])
// Check if the key is present
expect(actual[1]).toIncludeKey('payload')

Asserting sparse attributes and values by using a placeholder

Our goal is to achieve testing by specifing which values can be anything as long as they are present:

let asserted = [{
  type: FETCH_ORDERS,
  status: 'before',
  payload: {},
}, {
  type: FETCH_ORDERS,
  status: 'success',
  payload: Any
}]

expectLooseEquality(actual, asserted)

You can see that we marked the payload as a mandatory attribute which can have any value. That’s our placeholder. If we don’t care what the 2nd object should be then we can write the asserted value as:

let asserted = [{
  type: FETCH_ORDERS,
  status: 'before',
  payload: {},
}, Any]

and it should pass the assertion.

Assertion function that allows sparse attributes and values

The whole code for the assertion is:

import expect from 'expect'

export const Any = Symbol('Any')

export function expectLooseEquality(assert, actual) {
  expect.assert(compare(assert, actual), 'Expected %s to equal %s', assert, actual)
}

function compare(assert, actual) {
  if (actual === Any || assert === Any || actual === assert) {
    return true
  } else if (typeof assert === 'string') {
    return assert === actual
  } else if (isArray(assert) && isArray(actual)) {
    return compareArrays(assert, actual)
  } else if (isObject(assert) && isObject(actual)) {
    return compareObjects(assert, actual) && compareObjects(actual, assert)
  }
}

function isArray(obj) {
  return Object.prototype.toString.call(obj) === '[object Array]'
}

function isObject(obj) {
  var type = typeof obj
  return type === 'function' || type === 'object' && !!obj
}

function compareArrays(assert, actual) {
  if (assert.length !== actual.length) {
    return false
  } else {
    let allEqual = true

    for (let i = 0; i < assert.length; i++) {
      if (!compare(assert[i], actual[i])) {
        allEqual = false
        break
      }
    }

    return allEqual
  }
}

function compareObjects(assert, actual) {
  let allEqual = true

  for (let key in assert) {
    if (assert.hasOwnProperty(key)) {
      if (!compare(assert[key], actual[key])) {
        allEqual = false
        break
      }
    }
  }

  return allEqual
}

We define Any (line 3) as a symbol so that it will be unique (a plain object would work as well). Then we use this symbol whenever we need the attribute to be present but don’t care about its value.

Then we use expectLooseEquality (line 5) to make the assertion. It’s not the same syntax as expect provides, but it uses its assert method so it hooks nicely into the library.

This way you can save few lines of code and drastically improve tests readability.