Writing intuitive assertions

The core of any test is its assertions. An assertion prescribes exactly what we expect to occur, and so it is vital not only that we craft it accurately but that we craft it in a way that our expectation is made utterly clear. 

A single test will usually involve several assertions. And a test will typically follow the form of: given an input of X, do I receive an output of Y? Sometimes, establishing Y is complex and may not be constrained to a singular assertion. We may want to introspect Y to confirm that it is truly the desired output.

Consider a function named getActiveUsers(users)which will return only the active users from a set of all users. We may wish to make several assertions about its output:

const activeUsers = getActiveUsers([
{ name: 'Bob', active: false },
{ name: 'Sue', active: true },
{ name: 'Yin', active: true }
]);

assert(activeUsers.length === 2);
assert(activeUsers[0].name === 'Sue');
assert(activeUsers[1].name === 'Yin');

Here, we have clearly expressed our expectations for the output of getActiveUsers(...) as a series of assertions. Given a more fully-featured assertion library or more complex code, we could easily constrain this to a singular assertion, but it's arguably clearer to separate them.

Many testing libraries and utilities provide abstractions to aid us in making assertions. The popular testing libraries, Jasmine and Jest, for example, both provide a function called expect, which supplies an interface with many matchers, each individually allowing us to declare what characteristics a value should have, as in the following examples:

  • expect(x).toBe(y) asserts that x is the same as y
  • expect(x).toEqual(y) asserts that x is equal to y (similar to abstract equality)
  • expect(x).toBeTruthy() asserts that x is truthy (or Boolean(x) === true)
  • expect(x).toThrow() asserts that x, when invoked as a function, will throw an error

The exact implementation of these matchers may vary from library to library, and the abstraction and naming provided may also vary. Chai.js, for example, provides both the expect abstraction and a simplified assert abstraction, allowing you to assert things in the following fashion:

assert('foo' !== 'bar', 'foo is not bar');
assert(Array.isArray([]), 'empty arrays are arrays');

The most important thing when crafting an assertion is to be utterly clear. Just as with other code, it is unfortunately quite easy to write an assertion that is incomprehensible or hard to parse. Consider the following assertion:

chai.expect( someValue ).to.not.be.an('array').that.is.not.empty;

This statement, due to the abstractions provided by Chai.js, has the appearance of a human-readable and easily understandable assertion. But it is actually quite difficult to understand exactly what's going on. Let's consider which of the following this statement might be checking:

  • The item is not an array?
  • The item is not an empty array?
  • The item has a length greater than zero and is not an array?

It is, in fact, checking that the item is both not an array and that it is non-empty—meaning that, if the item is an object, it'll check that it has at least one property of its own, and if it's a string, it'll check that its length is greater than zero. These true underlying mechanics of the assertion are obscured and so, when exposed to such things, programmers may be left in a state of either blissful ignorance (thinking the assertion works as they wish it to) or painful confusion (wondering how on earth it works).

It may be the case that what we wished to assert all along was simply whether someValue was both not an array but was array-like, and as such, had a length greater than zero. As such, we can lend clarity using Chai.js's lengthOf method in a new assertion:

chai.expect( someValue ).to.not.be.an('array');
chai.expect( someValue ).to.have.a.lengthOf.above(0);

To avoid any doubt and confusion, we could, alternatively, assert more directly without relying on Chai.js's sentence-like abstractions:

assert(!Array.isArray(someValue), "someValue is not an array");
assert(someValue.length > 0, "someValue has a length greater than zero");

This is arguably far clearer as it explains to the programmer the exact check that is taking place, eliminating the doubt that could arise with a more abstract assertion style.

The crux of a good assertion is its clarity. Many libraries provide fancy and abstract mechanics of assertion (via the expect() interface, for example). These can create more clarity, but if over used, they can end up being less clear. Sometimes, we just need to Keep it Simple, Stupid (KISS). Testing code is the worst possible place in which to get fancy with egotistic or mis-abstracted code. Simple and straightforward code wins every time.

Now that we've explored the challenge of crafting intuitive assertions, we can slightly zoom out and have a look at how we should craft and structure the tests that contain them. The next section reveals hierarchies as a helpful mechanism to communicate meaning through our test suites.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.125.135