Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does the chai expect function work?

From chai's api you've got code like this:

.exist

Asserts that the target is neither null nor undefined.

var foo = 'hi'
  , bar = null
  , baz;

expect(foo).to.exist;
expect(bar).to.not.exist;
expect(baz).to.not.exist;

How does that exist part work? The expect function returns an object, then there's simply a property lookup on the "to" object. That's simply a property evaluation though isn't it? The only thing that would make sense to me is if the exist property is a getter method.

What's the go?

like image 670
Larry Lawless Avatar asked Jul 24 '15 08:07

Larry Lawless


2 Answers

chai exposes an use method to access the chai export and it's utils.

This method can be used by third parties when creating plugins, but it's also used internally to load it's interface.

The implementation of this method is simple:

exports.use = function (fn) {
  if (!~used.indexOf(fn)) {
    fn(this, util);
    used.push(fn);
  }

  return this;
};

Internally it uses this to load (among other) the primary Assertion prototype and the core assertion functionality:

var assertion = require('./chai/assertion'); // primary Assertion prototype
exports.use(assertion); // load it

var core = require('./chai/core/assertions'); // core assertion functionality
exports.use(core); // load it

One of the methods that are exposed by the Assertion prototype is the addProperty method which allows you to add properties to said prototype.

Internally chai uses this method to add the core assertion functionality to the Assertion prototype. For instance, all language chains and assertion helpers (exist, empty, etc) are added this way.

Language chains:

[ 'to', 'be', 'been'
  , 'is', 'and', 'has', 'have'
  , 'with', 'that', 'which', 'at'
  , 'of', 'same' ].forEach(function (chain) {
    Assertion.addProperty(chain, function () {
      return this;
    });
  });

All this functionality becomes available when a specific interface gets loaded internally, for instance expect. When this interface is loaded, a new Assertion prototype will be instantiated whenever expect gets executed, which will contain all functionality:

// load expect interface
var expect = require('./chai/interface/expect'); // expect interface
exports.use(expect); // load it

// expect interface
module.exports = function (chai, util) {
  chai.expect = function (val, message) {
    return new chai.Assertion(val, message); // return new Assertion Object with all functionality
  };
};

As you can see the expect method accepts a val argument (and an optional message argument). When this method is called (for instance expect(foo)) a new Assertion prototype will be instantiated and returned, exposing all core functionality (allowing you to do expect(foo).to.exist).

The Assertion Constructor uses the flag util to set a flag value on the Object that maps to the passed in val argument.

  function Assertion (obj, msg, stack) {
    flag(this, 'ssfi', stack || arguments.callee);
    flag(this, 'object', obj); // the 'object' flag maps to the passed in val
    flag(this, 'message', msg);
  }

All exist then does, is get this value through the flag util and evaluates if it not equals to null, using the assert method defined on the Assertion prototype.

  Assertion.addProperty('exist', function () {
    this.assert(
        null != flag(this, 'object')
      , 'expected #{this} to exist'
      , 'expected #{this} to not exist'
    );
  });
like image 114
danillouz Avatar answered Oct 10 '22 20:10

danillouz


When you call expect(foo), a new Assertion object is instantiated.

to, have, with, and similar properties do nothing but to return that Assertion instance. They are only for readability.

However, in your example, exists, is actually something that runs an assertion.

Its a property. They way properties are added to Assertion is that they are defined as getter functions as you can see here.

expect(foo).to.exist could be broken down to this:

const assertion = new Assertion;
assertion.exists;

assertion.exists is added to the assertion object with a getter. That means when you execute assertion.exists, to evaluate the value of assertion.exists, a function that was earlier provided to addProperty is executed.

You can read more about getter functions.

like image 20
Emil Sedgh Avatar answered Oct 10 '22 20:10

Emil Sedgh