One of the problems with “natural language” testing frameworks, or as I like to think of them, “semantic testing” frameworks, is that language is inherently ambiguous. This can lead to problematic instances where the developer (myself) thinks that, just because a test is written as an English sentence (“expect variable alpha to be a string equal to ‘abc’ and have length of 3”), my interpretation of how this is tested is the same as the frameworks. Often, it is not.
On to specifics: I hit this issue head on when I couldn’t understand why Chai was letting an array pass a test that it should not have been.
Chai – Making sure an Array does NOT include members
Click here to jump right to the solution, or read on for background and what does *not* work.
Problem and attempt:
Let’s start with the result we want. We want to ensure that {inputArr} (type of array), does not contain any of {blockArr} (type of array).
const blockArr = ['&', '@', '!'];
One of Chai’s handy chaining functions is the “not” method. Per the docs, this “negates all assertions that follow in the chain”. If we want to make sure that an array does not contain of the block elements, this seems like a good starting spot. So, we start with expect(inputArr).to.not
Next, lets combine this with the “.members()” method, which specifies array or object members to check against. So now we have expect(inputArr).to.not.include.members(blockArr)
. However, this will only fail if inputArr has every element of blockArr in it, so let’s use the “.any” modifier to specify that inputArr should not have any element of blockArr in it.
Here is what we ended up with:
expect(inputArr).to.not.include.any.members(blockArr);
That looks good, right? It should work? WRONG! It does not work! This is why I mentioned language ambiguity. This reads as a valid sentence, and uses valid methods from Chai, so why is it not working?
Well, the answer comes in reading more into the docs and realizing that there is a bad combination of requirements.
- We have to use
any
in the chain, because otherwise methods likeinclude
orcontains
expect that inputArr is a superset of blockArr. In practicality, this meansexpect(['a', 'b', 'c']).to.not.include.members(['a', 'b'])
will successfully fail, but if we change it toexpect(['a', 'b', 'c']).to.not.include.members(['a', 'e'])
, it lets the test pass instead of failing it.- This goes back to ambiguity. If I asked you “does the word DOG not contain the letters DB”, you might answer “Yes, it does not contain those letters”, or you might answer “No, it contains the letter D, but not the letter B”
- We are required by point 1 to use
any
, but Chai only applies this rule tokeys
, not tomembers
!
Because any
only works on keys, including it with negated members actually has zero effect.
Solution: Workaround to use any with array member values
First, the way that Chai seems to recommend is to just keep chaining more precise rules together, like so:
expect(inputArr).to.not.include('&').and.not.include('@').and.not.include('!');
But this starts to get ridiculous with long lists of blocking elements, and is not usable with dynamic tests. Here is a solution that keeps things simple and lets you check that inputArr does not contain ANY element from blockArr:
expect(inputArr.reduce((running, curr) => {
running[curr] = curr;
return running;
}, {})).to.not.include.any.keys(blockArr);
That’s it! All I’ve done is transformed inputArr from an array, to an object where the keys are the values in the array. This lets us use the any
chain method with keys
, so we don’t have any issues like we did with members
.
Keep in mind that this removes duplicates, but that shouldn’t really matter if you are trying to check that an array does not contain elements.
Another option is to just iterate over the input array and check for blocked elements, then store the result in a boolean, and then check the value of the boolean, like so:
const foundBlocked = inputArr.reduce((blocked, current) => {
return blocked || blockArr.indexOf(current) !== -1;
}, false);
expect(foundBlocked).to.equal(false);
Proof and demo:
If you want to see how this all works, and maybe play with the results yourself, check out this Repl I set up – https://repl.it/@joshuatz/Chai-Array-Does-Not-Include-Any-Members