Assertions are mainly used for “debugging” and “testing”

Assertions in the front end

A close look at the API in JavaScript shows that there aren’t really many methods for assertions. The only one is console.assert:

  // console.assert(condition, message)
  const a = '1'
  console.assert(typeof a === 'number'.'a should be Number')
Copy the code

When condition is false, this method writes an error message to the console. If true, there is no response.

In fact, console.assert is rarely used, and if you look at open source projects like Vue or Vuex, you’ll see that they all have custom assert methods:

  // Vuex source tool functions
  function assert (condition, msg) {
    if(! condition) {throw new Error(`[Vuex] ${msg}`)}}Copy the code

2. Assertions in Node

Node has a built-in assert library. Here’s a simple example:

  try {
    assert(false.'This value should be true')}catch(e) {
    console.log(e instanceof assert.AssertionError) // true
    const { actual, expected, operator } = e
    console.log('Actual value:${actual}, expected value:${expected}, using the operator:${operator}`)
    // Actual value: false, expected value: true, operator used: ==
  }
Copy the code

The assert module provides several methods, such as strictEqual, deepStrictEqual, notDeepStrictEqual, etc.

  • Abstract Equality comparison algorithm (==)
  • Strict equality comparison algorithm (===)
  • SameValue (Object.is())
  • SameValueZero

This is something you may have missed while studying ES7.

In the Node10.2.0 documentation you’ll find apis like assert.equal and assert.deepequal have been abolished to avoid the error-prone complexity of ==. The remaining API basically adopts the latter algorithms, for example:

  • StrictEqual uses a strict comparison algorithm
  • DeepStrictEqual uses the SameValue algorithm to compare raw values

Third, chai. Js

As you can see from the examples above, the assertion methods built into JavaScript are not particularly comprehensive, so here we can select some tripartite libraries to meet our needs.

Here we can choose chai. Js, which supports two styles of assertion (TDD and BDD) :

  const chai = require('chai')
  const assert = chai.assert
  const should = chai.should()
  const expect = chai.expect

  const foo = 'foo'

  // TDD style assert
  assert.typeOf(foo, 'string')

  // BDD style should
  foo.should.be.a('string')

  // BDD style expect
  expect(foo).to.be.a('string')
Copy the code

Most people opt for the Expect assertion library, and it does feel good to use. You can check the official documents for details. After all, you can only choose the right library after checking your eyes.

Four, expect. Js source code analysis

Not only does Expect. Js provide rich invocation methods, but more importantly, it provides a natural language-like chain invocation.

Chain calls

When it comes to chained calls, we typically use the method that returns this in a function that requires a chained call:

  class Person {
    constructor (name, age) {
      this.name = name
      this.age = age
    }
    updateName (val) {
      this.name = val
      return this
    }
    updateAge (val) {
      this.age = val
      return this
    }
    sayHi () {
      console.log(`my name is The ${this.name}.The ${this.age} years old`)}}const p = new Person({ name: 'xiaoyun'.age: 10 })

  p.updateAge(12).updateName('xiao ming').sayHi()
Copy the code

In Expect. Js, however, we don’t just use chained calls like this. First, we know that Expect is actually an instance of an Assertion:

  function expect (obj) {
    return new Assertion(obj)
  }
Copy the code

Now look at the core Assertion constructor:

  function Assertion (obj, flag, parent) {
    this.obj = obj;
    this.flags = {};

    // Use flags to record the tokens used in the chain call,
    // The final result will be determined by checking whether the value of not in flags is true
    if (undefined! = parent) {this.flags[flag] = true;

      for (var i in parent.flags) {
        if (parent.flags.hasOwnProperty(i)) {
          this.flags[i] = true; }}}// Recursively register an Assertion instance, so Expect is a nested object
    var $flags = flag ? flags[flag] : keys(flags)
      , self = this;
    if ($flags) {
      for (var i = 0, l = $flags.length; i < l; i++) {
        // Avoid an endless loop
        if (this.flags[$flags[i]]) {
          continue
        }

        var name = $flags[i]
          , assertion = new Assertion(this.obj, name, this)
        
        // Understand that some of the modifiers are also methods on the Assertion prototype, e.g. An, be.
        if ('function'= =typeof Assertion.prototype[name]) {
          // The method on the clone prototype
          var old = this[name];
          this[name] = function () {
            return old.apply(self, arguments);
          };

          // Because this is a function object, you can't find an Assertion on the Assertion prototype if you're chained to it.
          // So set all the methods on the Assertion prototype chain to the current object
          for (var fn in Assertion.prototype) {
            if(Assertion.prototype.hasOwnProperty(fn) && fn ! = name) {this[name][fn] = bind(assertion[fn], assertion); }}}else {
          this[name] = assertion; }}}}Copy the code

Why is it designed this way? My understanding is: First of all, the chain call of expect. Js fully reflects the logic of the call, and this nested structure really reflects the logic between each modifier.

So we can write it like this:

  const student = {
    name: 'xiaoming'.age: 20
  }

  expect(student).to.be.a('object')
Copy the code

There is more to assert than Assertion. For each Assertion on the Assertion prototype, you can call assert directly or indirectly:

  Assertion.prototype.assert = function (truth, msg, error, expected) {
    // This is what the flags attribute does
    var msg = this.flags.not ? error : msg
      , ok = this.flags.not ? ! truth : truth , err;if(! ok) {// Throw an error
      err = new Error(msg.call(this));
      if (arguments.length > 3) {
        err.actual = this.obj;
        err.expected = expected;
        err.showDiff = true;
      }
      throw err;
    }

    // Why create an Assertion instance? It is also because expect instances are nested objects.
    this.and = new Assertion(this.obj);
  };
Copy the code

And each method on the Assertion prototype ends up making a chained call by returning this. So we can also write:

  expect(student).to.be.a('object').and.to.have.property('name')
Copy the code

At this point you should have an idea of how expect. Js’s chain calls work, which can be summed up in two points:

  • The prototype method does the chain call again by returning this;
  • Enhance the logic of chain calls through nested structured instance objects;

So we could have written this:

  // How else can you be BDD style?
  expect(student).a('object').property('name')
Copy the code

If you like this article, please pay attention to my subscription number. I love typing code and check out more content.