One, foreword

Assertion library is an important part of unit test. When writing unit test code, assertion library is used to describe the expected effect of code logic, so as to verify the correctness of code logic.

The assertion library is generally divided into two categories: TDD (Test Driven Development) and BDD (Behavior Driven Development).

  • TDD: assert.js
  • BDD: should. Js and expect. Js

The main difference between BDD and TDD is that TDD addresses code-level validation, but does not address the problem of test code meeting requirements very well, while BDD involves more actors than developers in reviewing use cases to ensure that requirements match the test code.

Here is sample code for the TDD style assertion library assert.js:

  assert.typeOf(foo, 'string');

  assert.equal(foo, 'bar');

  assert.lengthOf(foo, 3)

  assert.property(tea, 'flavors');

  assert.lengthOf(tea.flavors, 3);

Copy the code

Look again at the sample code for the BDD style assertion library should.js:

  foo.should.be.a('string');

  foo.should.equal('bar');

  foo.should.have.lengthOf(3);

  tea.should.have.property('flavors').with.lengthOf(3);

Copy the code

By contrast, assertions are more readable and easier for non-developers to understand.

Next, this article will take you to explore the implementation principle of should.js.

Extend object properties

const num = 10;

num.should.be.above(8);

Copy the code

When we use should.js, it will mount a property named should for all objects. This relies on the attribute lookup mechanism in JavaScript to extend the Object’s prototype Object using the object.defineProperty method:

class Assertion {

  // ...

};



const should = (obj) = > {

  return new Assertion(obj);

};



Object.defineProperty(Object.prototype, 'should', {

  set() {},

  get() {

    return should(this);

  },

  configurabletrue

})

Copy the code

The Object.defineProperty method requires manual robustness and throws a TypeError when the property is not successfully defined. In contrast, using the metaprogramming static method reflect.defineProperty provided by ES6 solves this problem:

const ans1 = Reflect.defineProperty(Object.prototype, 'should', {

  set() {},

  get() {

    return should(this);

  },

  configurablefalse.

})



const ans2 = Reflect.defineProperty(Object.prototype, 'should', {

  set() {},

  get() {

    return should(this);

  },

})



console.log(ans1, ans2); // true false

Copy the code

In the above example, the should property is set to false for the first time. When the descriptor of the should property is set again, TypeError is not raised and the false value is returned to inform the developer that the operation fails.

Add chain calls

Chained calls are implemented primarily by returning the current instance in a method:

const should = (obj) = > {

  return new Assertion(obj);

};

Copy the code

This allows you to continue calling properties or methods on the current instance.

4. Assertion method mechanism

class Assertion {

  constructor(obj) {

    this.obj = obj;

  }



  above (n) {

    return this.assert(this.obj > n);

  }



  assert (expr) {

    if (expr) {

      return console.log('success');

    }

    return console.log('fail');

  }

};

Copy the code

When you extend the should attribute with the reflect.defineProperty method, you return an instance of the Assertion class on which Assertion methods such as Above are mounted, Internally, however, they use the Assert method to determine whether an expression’s return value is true or false.

Five, optimization assertion failure prompt

const { AssertionError } = require('assert');



class Assertion {

  constructor(obj) {

    this.obj = obj;

  }



  above (n, message) {

    this.params = { operator'should be above ' + n, message };

    return this.assert(this.obj > n);

  }



  assert (expr) {

    if (expr) {

      return console.log('success');

    }

    let { message } = this.params;

    if(! message) {

      message = `expected The ${this.obj} The ${this.params.operator}`

    }



    const err = new AssertionError({

      message,

      actualthis.obj,

      stackStartFnthis.assert,

    })



    throw err;

  }

};

Copy the code

The AssertionError class provided by Node.js is used here to display the function call stack and friendly messages for assertion failures. In the case that the developer does not manually pass in the message, a back-pocket copy can be constructed based on the externally provided method to improve the development experience.

6. Structural particles

num.should.be.above(8);

Copy the code

Structural particles such as be are used to enhance the readability of the assertion without affecting the logic of the assertion.

You can also use the reflect.defineProperty method, but note that the should attribute returns an instance of an Assertion, so these attributes should be extended to the Assertion stereotype attribute.

['an'.'of'.'a'.'and'.'be'.'have'.'with'.'is'.'which'.'the'].forEach(name= > {

  Reflect.defineProperty(Assertion.prototype, name, {

    get () {

      return this;

    }

  })

})

Copy the code

7. Negative attributes

class Assertion {

  constructor(obj) {

    this.obj = obj;

  }



  assert (expr) {

    let actualExpr = this.negate ? ! expr : expr;



    // Non-critical code omitted

  }



  get not () {

    this.negate = !this.negate

    return this;

  }

}

Copy the code

Add the negate attribute on the example to mark the current state. Negate = false. Negate = false. Negate = false.

const num = 20;

num.should.not.not.be.above(10);

Copy the code

Viii. Plug-in system

As the application scenario gets more complex, we can add more methods to the Assertion class, which has two drawbacks:

  • Assertion classes are very intrusive and add additional maintenance costs.
  • Package size problem
// Plugin registration method

should.use = function(plugin{

  plugin(Assertion);

  return this;

}



Assertion.add = function(name, fn{

  Object.defineProperty(Assertion.prototype, name, {

    value () {

      fn.apply(this.arguments);

    }

  })

}



function numberPlugin(Assertion{

  Assertion.add('above'.function (n, message{

    this.params = { operator'to be above ' + n, message };

    return this.assert(this.obj > n);

  });



  Assertion.add('below'.function(n, message{

    this.params = { operator'to be below ' + n, message };

    return this.assert(this.obj < n);

  });

}

// Register the plug-in

should.use(numberPlugin);

Copy the code

The Assertion method is decoupled from the Assertion class through the plug-in mechanism. And developers can tailor assertions to their own usage scenarios to address the package size issue.

Write at the end

The above is the content of this article, I hope to bring you help, welcome to “attention”, “like”, “forward”.