Table of contents
1.
Introduction
2.
Project Setup
3.
Constructing Language Chains
3.1.
Adding Properties
3.2.
Adding Methods
3.3.
Program
3.4.
Methods as Properties
3.4.1.
Program
3.4.2.
Output
4.
Example 
4.1.
Program
5.
Overwriting Language Chains
5.1.
Overwriting Properties
5.1.1.
Program
5.2.
Overwrite Structure
5.2.1.
Program
5.3.
Overwriting Methods
5.3.1.
Program
6.
FAQs
7.
Key Takeaways
Last Updated: Mar 27, 2024
Easy

Building Helper in Chai

Author Aman Thakur
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

This article assumes that you are having familiarity with the fundamental ideas of the plugin. If you haven't already done so, I strongly advise you to do so before proceeding, for that you can refer to the original documentation of chaiJs on plugins. 

The most typical usage of Chai's plugin utilities is to provide chainable helper assertions. Before we get into the fundamentals, we'll need a topic to which we can apply Chai's assertions to understand better. We will use a very basic data model object for this. Let’s get started with the helper method in Chai.

Project Setup

function Model(type) {
 this._type = type;
 this._attrs = {};
}

Model.prototype.set = function (key, value) {
 this._attrs[key] = value;
};

Model.prototype.get = function (key) {
 return this._attrs[key];
};
You can also try this code with Online Javascript Compiler
Run Code

In practice, this might be any data model object returned from an ORM database in node or built in the browser using your preferred MVC framework.

Our Model class should be self-explanatory, but we'll create a company object as an example.

var codingninjas = new Model('company');
codingninjas.set('name', 'Coding Ninjas');
codingninjas.set('type', 'Ed-tech');
console.log(codingninjas.get('name'));
You can also try this code with Online Javascript Compiler
Run Code

Now that we've established our topic, we can move on to the fundamentals of plugins.

Constructing Language Chains

We've arrived at the exciting part! Chai's plugin API is used for adding properties and functions.

Adding Properties

In essence, you may define a property using Object.defineProperty, but we recommend that you utilise Chai's utility helpers to guarantee a consistent implementation.
 

var codingninjas = new Model('company');
expect(codingninjas).to.be.a.model;
You can also try this code with Online Javascript Compiler
Run Code

We'll utilise the addProperty tool for this.

utils.addProperty(Assertion.prototype, "model", function () {
 this.assert(
   this._obj instanceof Model,
   "expected #{this} to be a Model",
   "expected #{this} to not be a Model"
 );
});
You can also try this code with Online Javascript Compiler
Run Code

Simple and to the point. From here, Chai may take over. It's also worth noting that Chai makes this extension pattern a little simpler because it's so common. The following can be substituted for the first line:

Assertion.addProperty('model', function () { // ...
You can also try this code with Online Javascript Compiler
Run Code

All chain extension utilities are included in the utils object and are also available on the Assertion function Object() { [native code] }. The methods will be called straight from Assertion throughout the rest of this text.

Adding Methods

Multiple plugins that use addMethod to define the same method name will clash, with the last-registered plugin prevailing. The plugin API is slated for a big redesign in future versions of Chai, which will address this contradiction among other things. In the meanwhile, please use overwriteMethod instead.

Though a property is a nice idea, it's probably not detailed enough for the assistance we're making. Because our models have types, it would be useful to state that our model belongs to one of them. We'll need a technique for this.

Program

// goal
expect(codingninjas).to.be.a.model("company");

// language chain method
Assertion.addMethod("model", function (type) {
 var obj = this._obj;

 // first, our instanceof check, shortcut
 new Assertion(this._obj).to.be.instanceof(Model);

 // second, our type check
 this.assert(
   obj._type === type,
   "expected #{this} to be of company type #{exp} but got #{act}",
   "expected #{this} to not be of company type #{act}",
   type, // expected
   obj._type // actual
 );
});
You can also try this code with Online Javascript Compiler
Run Code

Because all assert calls are synchronous, if the first one fails, an AssertionError is produced, and the second one will be skipped. The test runner is responsible for deciphering the message and handling the presentation of any failed assertions.

Methods as Properties

Chai has a special feature that allows you to create a language chain that can be used as a property or a method. These are referred to as "chainable approaches." Despite the fact that the "is model of model" property and method have been shown, these assertions are not an acceptable use case for chainable methods. 

We'll look at a chainable method from Chai's core to see when it's the optimal time to utilise it.

Program

var numberArray = [1, 2, 3],
 object = { a: 1, b: 2 };

it("Array contains the value and Key", () => {
 expect(numberArray).to.contain(2);
 expect(object).to.contain.key("a");
});
You can also try this code with Online Javascript Compiler
Run Code

Output

Two distinct functions are required for this to operate. One is called when the chain is used as a property or a method, while the other is called when the chain is solely used as a method.

The only role of contain as a property in these examples, and with all of the other chainable methods in core, is to set a contains flag to true.This tells keys to act in a different way. When key is used with contain in this situation, it will check for the inclusion of a key rather than testing for an exact match to all keys.

Let's pretend we create a chainable method for model that does what we want it to do: do an instanceof check if it's a property, and a _type check if it's a method. There would be a disagreement as a result of this...

The following might be appropriate

expect(codingninjas).to.be.a.model;
expect(codingninjas).to.be.a.model('company');
expect(arr).to.not.be.a.model;
You can also try this code with Online Javascript Compiler
Run Code

The following, on the other hand, would not

expect(codingninjas).to.not.be.a.model('company');
You can also try this code with Online Javascript Compiler
Run Code

Because the property assertion is called when the function is also used as a method, and negation affects ALL assertions once it is set, we'd get an error message like anticipated [object Model] not to be an instance of [object Model]. As a result, while developing chainable procedures, please follow this general rule.

The property function should only be used to establish a flag for subsequently altering the behaviour of an existing assertion when creating chainable methods.

Example 

We'll build an example that allows us to test Company's age accurately, or chain into Chai's numerical comparators, such as above, below, and inside, for usage with our model example. You'll need to learn how to rewrite methods without breaking the program's essential functionality, but that'll come later.

All of the following will be possible if we achieve our objective.

expect(codingninjas).to.have.age(27);
expect(codingninjas).to.have.age.above(17);
expect(codingninjas).to.not.have.age.below(18);
You can also try this code with Online Javascript Compiler
Run Code

Let's begin by putting together the two functions required for a chainable method. The function to employ when invoking the age method comes first.

Program

function assertModelAge (n) {

 new Assertion(this._obj).to.be.instanceof(Model);
 var companyAge = this._obj.get('age');
 new Assertion(companyAge).to.be.a('number');

 this.assert(
     companyAge === n
   , "expected #{this} to have #{exp} but got #{act}"
   , "expected #{this} to not have age #{act}"
   , n
   , companyAge
 );
}
You can also try this code with Online Javascript Compiler
Run Code

That should be self-explanatory by now.Now it's time to look at our property function.

function chainModelAge () {
 utils.flag(this, 'model.companyAge', true);
}
You can also try this code with Online Javascript Compiler
Run Code

Later on, we'll train our numerical comparators to search for that flag and adjust their behaviour when they see it. We'll need to securely override that method since we don't want to damage the core methods.

Assertion.addChainableMethod('age', assertModelAge, chainModelAge);
You can also try this code with Online Javascript Compiler
Run Code

Done.We can now determine Company's actual age. We'll return to this example when we learn how to override methods.

Overwriting Language Chains

We should work on being able to properly rewrite existing assertions, such as those from Chai's core or other plugins, now that we've successfully added assertions to the language chain.

Chai offers a variety of tools that allow you to rewrite current assertion behaviour while reverting to the previously specified behaviour if the assertion's topic does not satisfy your requirements.

Let's start with a basic example of property overwriting.

Overwriting Properties

We'll replace the ok property offered by Chai's core in this example. If an object is truthy, the default behaviour is for ok to pass. We want to tweak that behaviour such that using ok with a model instance confirms that the model is fully formed. We'll consider a model acceptable if it has an id attribute in our example.

Let's begin with a simple overwrite utility and a simple assertion.

Program

chai.overwriteProperty("ok", function (_super) {
 return function checkModel() {
   var obj = this._obj;
   if (obj && obj instanceof Model) {
     new Assertion(obj).to.have.deep.property("_attrs.id").a("number");
   } else {
     _super.call(this);
   }
 };
});
You can also try this code with Online Javascript Compiler
Run Code

Overwrite Structure

The primary difference in overwriting, as you can see, is that the first function only sends one _super parameter. This is the function that was previously available, and you should use it if your criteria don't match. Second, you'll see that we return a new function right away, which will be used to make the real assertion.

We can now write affirmative claims with this in place.

var codingninjas = new Model('company');
codingninjas.set('id', 42);
expect(company).to.be.ok;
expect(true).to.be.ok;
You can also try this code with Online Javascript Compiler
Run Code

The above-mentioned expectations will be met. When dealing with models, it will use our own assertion; when working with non-models, it will use the default behaviour.

var codingninjas = new Model('company');
codingninjas.set('id', 'dont panic');
expect(codingninjas).to.not.be.ok;
You can also try this code with Online Javascript Compiler
Run Code

Because our assertion is negated and the id is not a number, we anticipate this expectation to be fulfilled as well. Unfortunately, our number assertion was not given the negation flag, so it still expects the value to be a number.

This assertion will be expanded by moving all of the flags from the previous assertion to our new assertion. This is how the final property overwrite would look.

Program

chai.overwriteProperty("ok", function (_super) {
 return function checkModel() {
   var obj = this._obj;
   if (obj && obj instanceof Model) {
     new Assertion(obj).to.have.deep.property("_attrs.id");
     var assertId = new Assertion(obj._attrs.id);
     utils.transferFlags(this, assertId, false);
     assertId.is.a("number");
   } else {
     _super.call(this);
   }
 };
});
You can also try this code with Online Javascript Compiler
Run Code

We do need to make one more minor change, though. If our assertion fails because the id property is the wrong type, we'll get an error notice that says 'don't panic' should [not] be a number.We'll give it a little more information because it's not very useful when running a huge test suite.

var assertiontId = new Assertion(obj._attrs.id, 'model assert ok for given id type');
You can also try this code with Online Javascript Compiler
Run Code

Overwriting Methods

Overwriting techniques are structured similarly to overwriting properties. We'll use the same example as before, stating company's age to be greater than a certain threshold.

var codingninjas = new Model('company');
codingninjas.set('companyAge', 6);
expect(codingninjas).to.have.age.above(6);
You can also try this code with Online Javascript Compiler
Run Code

We already have an age chain set up to mark the assertion using model.age, so we have to check now if it exists.

Program

Assertion.overwriteMethod("above", function (_super) {
 return function assertAge(n) {
   if (utils.flag(this, "model.age")) {
     var obj = this._obj;
     new Assertion(obj).instanceof(Model);

     new Assertion(obj).to.have.deep.property("_attrs.age").a("number");

     var companyAge = obj.get("companyAge");
     this.assert(
       companyAge > n,
       "expected #{this} for company age above #{exp} but got #{act}",
       "expected #{this} to not have for company age above #{exp} but got #{act}",
       n,
       companyAge
     );
   } else {
     _super.apply(this, arguments);
   }
 };
});
You can also try this code with Online Javascript Compiler
Run Code

This includes both optimistic and pessimistic situations. There is no need to transmit flags in this scenario. Assert takes care of it automatically. The similar design may be used below and within as well.

FAQs

1. What assertion styles are present in Chai testing assertion library?
Ans: Chai is an assertion library that offers certain interfaces for any JavaScript-based framework to implement assertions. TDD styles and BDD styles are the two types of interfaces used by Chai.

2. What is chai code?
Ans: Chai is an assertion library that supports both BDD and TDD programming techniques, allowing you to test your code in any testing environment. Using Chai's expect interface, we'll focus on the BDD style throughout this tutorial.

3. What is Sinon chai?
Ans: With the Chai assertion library, Sinon–Chai provides a set of custom assertions for using the Sinon.JS spy, stub, and mocking framework. You get all of the benefits of Chai plus all of Sinon's amazing tools.

4. What is assertion in Cypress?
Ans: Assertions are checkpoints in an automated test case that confirm if a test step passed or failed. As a result, it verifies that the application under test is in the intended condition. For assertions, Cypress includes the Chai, JQuery, and Sinon libraries.

Key Takeaways

If you reached till here, that means you really find this article interesting, here are some insights which sums up the overall article. The importance of utilising Chai's plugin utilities is most commonly used to offer chainable helper assertions and Chai's assertions. Which helps in either building the helper chains or overwriting them as per needed.

You might be interested in articles such as Fundamentals of Software TestingDeep-Eql utilities and check-errors in chai. If that not enough you can just, head over to our practice platform Coding Ninjas Studio to practise top problems, attempt mock tests, read interview experiences, and much more.

Happy Learning! 

Live masterclass