RoboticRenaissance
RoboticRenaissance

Reputation: 1187

How can I assert that a subset of an object exists in a unit test?

So I have a big nested list of strings. The big nested list will eventually get bigger. But I don't want the unit tests to change when the big nested list gets bigger.

I'm returning custom errors and jazz. But that's irrelevant to this question. So I'm going to abstract this to a list of fruit.

What I want is to be able to add items to the bigger list without disrupting the unit test. In other words, I want to see if the complete/haystack/superset includes the partial/needle/subset. (so no deepEquals). I've tried a lot of different ways to do this, and I keep coming up short.

mocha spec file

import "chai/register-assert";
import { assert } from "chai";

describe.only("Nested Lists and Objects", () => {
  let allFoodList = {
    smoothieStuff: ["apples", "bananas", "strawberries", "yogurt", "ice"],
    saladStuff: ["apples", "carrots", "spinach", "strawberries"],
    saladFruit: ["apples", "strawberries"],
  };

  let fruitList = {
    smoothieStuff: ["apples", "bananas", "strawberries"],
    saladStuff: ["apples", "strawberries"],
    saladFruit: ["apples", "strawberries"],
  };

  // None of these work
  it("deepNestedInclude", () => {
    assert.deepNestedInclude(allFoodList, fruitList);
  });
  it("deepInclude", () => {
    assert.deepInclude(allFoodList, fruitList);
  });
  it("include", () => {
    assert.include(allFoodList, fruitList);
  });
  it.skip("notInclude", () => {
    assert.notInclude(allFoodList, fruitList);
  });
  it("deepInclude", () => {
    assert.deepInclude(allFoodList, fruitList);
  });
  it.skip("notDeepInclude", () => {
    assert.notDeepInclude(allFoodList, fruitList);
  });
  it("nestedInclude", () => {
    assert.nestedInclude(allFoodList, fruitList);
  });
  it("deepNestedInclude", () => {
    assert.deepNestedInclude(allFoodList, fruitList);
  });
  it("ownInclude", () => {
    assert.ownInclude(allFoodList, fruitList);
  });
  it.skip("notOwnInclude", () => {
    assert.notOwnInclude(allFoodList, fruitList);
  });
  it("deepOwnInclude", () => {
    assert.deepOwnInclude(allFoodList, fruitList);
  });
  it.skip("notDeepOwnInclude", () => {
    assert.notDeepOwnInclude(allFoodList, fruitList);
  });
  it("sameMembers", () => {
    assert.sameMembers(allFoodList, fruitList);
  });
  it("sameDeepMembers", () => {
    assert.sameDeepMembers(allFoodList, fruitList);
  });
  it("includeMembers", () => {
    assert.include(allFoodList, fruitList);
  });
  it("includeDeepMembers", () => {
    assert.include(allFoodList, fruitList);
  });
});

summary

  0 passing (22ms)
  4 pending
  12 failing

I skipped the 'Not' tests because they would also pass for something that doesn't contain the required items. I also didn't put in anything that said "ordered" because I'm pretty sure that's an extra constraint.

Upvotes: 2

Views: 1325

Answers (3)

revelt
revelt

Reputation: 2400

There are plenty of npm packages that compare plain objects, I wrote one too, ast-compare.

For context, I came here wanting to double-check, does a native shallow comparison solution exists on Node Assert, apparently it does not. I'm using uvu test runner which is very spartan and it ships with only deep equal comparison.

Upvotes: 0

RoboticRenaissance
RoboticRenaissance

Reputation: 1187

This is not the answer I agree with, but it's the answer I'm going with if I don't get something better.

I appreciate Scott Sauyet's answer, but it falls a little short. I knew I should have given a more detailed example, but I was tired and frustrated. The problem is that the arrays and objects can be rather deeply nested. I ended up going with a recursive function.

I am open to critique and advice on how to make this better/more robust.

Explanation:

  • The try/catch blocks are there to say "If any of these do work, return true, we're done here." If they don't work, it just falls through to the next comparison.
  • Then, if there are objects involved, rather than arrays, we go through the properties on the needle and run the similar comparison on each property of the needle.
  • Currently not checking for all infinite loops. Just some. Partially because I'm testing JSON returned from an API, so circular reference in objects isn't really a problem I'm going to encounter. But "something"[0][0][0][0][0] does exist. "s"[0]==="s".
  • If there are not objects involved, just compare needle and haystack for loose equality. If they are equal, great. No error.

homebrew.assert.js

import { assert } from "chai";

function nestedInclude(haystack, needle, path) {
  try {
    assert.deepEqual(haystack, needle);
    return true;
  } catch {}
  try {
    assert.deepNestedInclude(haystack, needle);
    return true;
  } catch {}
  try {
    assert.includeDeepMembers(haystack, needle);
    return true;
  } catch {}
  if (typeof haystack === "object" || typeof needle === "object") {
    for (let key in needle) {
      let val = needle[key];
      if (val === needle) {
        throw new Error("We just entered an infinite loop.  Great.");
      }
      let subPath = path + "." + key;
      nestedInclude(haystack[key], needle[key], subPath);
    }
    return true;
  } else {
    assert.deepEqual(haystack, needle);
  }
}

export { nestedInclude };

export default {
  nestedInclude,
};

usage

import homebrew from "~/test/homebrew.assertion.js";

describe("...", ()=>{
  /* ... */
  it("...", ()=>{
    homebrew.nestedInclude(allFoodList, fruitList);
  });
});

Upvotes: 0

Scott Sauyet
Scott Sauyet

Reputation: 50787

Can't you just use some helper functions?:

const isSubset = (haystack, needles) =>
  needles .every (needle => haystack .includes (needle))

const allAreSubsets = (haystackObj, needleObj) =>
  Object .keys (needleObj) .every (key => isSubset (haystackObj[key] || [], needleObj[key]))

const allFoodList = {
  smoothieStuff: ["apples", "bananas", "strawberries", "yogurt", "ice"],
  saladStuff: ["apples", "carrots", "spinach", "strawberries"],
  saladFruit: ["apples", "strawberries"],
}

const fruitList = {
  smoothieStuff: ["apples", "bananas", "strawberries"],
  saladStuff: ["apples", "strawberries"],
  saladFruit: ["apples", "strawberries"],
}

const extraFruit = {
  smoothieStuff: ["apples", "bananas", "strawberries"],
  saladStuff: ["apples", "strawberries"],
  saladFruit: ["apples", "strawberries", "kiwis"],
}

console .log (allAreSubsets (allFoodList, fruitList))
console .log (allAreSubsets (allFoodList, extraFruit))

isSubset just tells if everything in needles is also in haystack. allAreSubsets does the same thing for every property of needlesObj, comparing it to the same property of haystackObj (or to an empty array if it's not there.)

Including something like that, you should be able to make simple assertions about the boolean results.

Upvotes: 1

Related Questions