Understanding matchers

By now, you've already seen plenty of usage examples for matchers, and probably can feel how they work.

You have seen how to use the toBe , the toEqual , and toBeTruthy matchers. These are a few of the built-in matchers available in Jasmine, but we can extend Jasmine by writing matchers of our own.

So, to really understand how Jasmine matchers work, we need to create one ourselves.

Custom matchers

Consider this expectation from the previous section:

expect(investment.isGood()).toBeTruthy();

Although it works, it is not very expressive. Imagine if we could rewrite it instead, as:

expect(investment).toBeAGoodInvestment();

This makes a much better relation to the acceptance criterion:

"should be a good investment"→expect investment to be a good investment

And to implement it is quite simple. You do so by calling the this.addMatcher Jasmine function inside a setup (beforeEach) or a spec (it).

Although you can put this new matcher definition inside the spec/InvestmentSpec.js file, Jasmine already has a default place to add custom matchers, the file spec/SpecHelper.js. If you are using the Standalone Distribution, it already comes with a sample custom matcher; delete it and let's start from scratch.

The addMatcher function accepts a single parameter—an object where each attribute corresponds to a new matcher. So to add this new matcher, change the contents of the spec/SpecHelper.js file to:

beforeEach(function() {
  this.addMatchers({
    toBeAGoodInvestment: function() {}
  });
});

A Jasmine matcher is simply a function that returns a Boolean value: true, to indicate that the expectation is passing and false if otherwise.

But to implement this matcher, we need access to the investment object, available via the this.actual property:

toBeAGoodInvestment: function() {
  var investment = this.actual;
  return investment.isGood();
};

After getting access to the investment object, implementing the matcher was a simple return of the isGood() value.

By now, this matcher is ready to be used by the specs:

it("should be a good investment", function() {
  expect(investment).toBeAGoodInvestment();
});

After the change, the specs should still be passing. But what happens if a spec fails? What is the error message that Jasmine reports?

We can see it by deliberately breaking the investment.isGood implementation in src/Investment.js, to always return false:

Investment.prototype.isGood = function() {
  return false;
};

When running the specs again, this is the error message that Jasmine generates:

Custom matchers

Custom matcher's message

It is not so bad, but sure can be made better. Jasmine allows the customization of this message via the this.message property inside the matcher declaration. Jasmine expects this property to be a function that returns the error message:

toBeAGoodInvestment: function() {
  var investment = this.actual;

  this.message = function() {
    return 'Expected investment to be a good investment';
  };

  return investment.isGood();
}

Run the specs again and the error message should change:

Custom matchers

Custom matcher's custom message

Now, let's consider another acceptance criterion:

"Given an investment, when its stock share price devalorizes, it should be a bad investment".

Although it is possible to create a new custom matcher (toBeABadInvestment), Jasmine allows the negation of any matcher by chaining not before the matcher call. So we can write that a "bad investment" is "not a good investment":

expect(investment).not.toBeAGoodInvestment();

Implement this new acceptance criterion in spec/InvestmentSpec.js by adding a new nested describe and spec:

describe("when its stock share price devalorizes", function() {
  beforeEach(function() {
    stock.sharePrice = 0;
  });

  it("should be a bad investment", function() {
    expect(investment).not.toBeAGoodInvestment();
  });
});

But there is a catch! Let's break the Investment code so that it is always a good investment:

Investment.prototype.isGood = function() {
  return true;
};

After running the specs again, you can see that this new spec is failing, but the error message is wrong: Expected investment to be a good investment.

Custom matchers

Custom matcher's wrong custom negated message

That is the message that was hard coded inside the matcher. To fix this, you need to make the message dynamic, based on how the matcher was called.

Luckily there is a property available inside the matcher declaration that tells if the matcher was called with a chained not, this.isNot.

Following is the fixed matcher with the dynamic message:

toBeAGoodInvestment: function() {
  var investment = this.actual;
  var what = this.isNot ? 'bad' : 'good';

  this.message = function() {
    return 'Expected investment to be a '+ what +' investment';
  };

  return investment.isGood();
}

This fixes the message:

Custom matchers

Custom matcher's custom dynamic message

And now this matcher can be used anywhere.

What lacked in this example was a way to show how to pass an expected value to a matcher like this:

expect(investment.cost).toEqual(2000)

It turns out that a matcher can receive any number of expected values as parameters. So for instance, the preceding matcher could be implemented as:

toEqual: function(expectedValue) {
  return this.actual == expectedValue;
};

Always before implementing any matcher, check first if there is one available that already does what you want.

Built-in matchers

Jasmine comes with a bunch of default matchers, covering the basis of value checking in the JavaScript language. To understand how they work and where to use them properly is a journey on how JavaScript handles type.

The toEqual built-in matcher

This is probably the most commonly used matcher, and you should use it whenever you want to check equality between two values.

It works for all primitive values (number, string, and Boolean) as well as any object (including arrays).

describe("toEqual", function() {
  it("should pass equal numbers", function() {
    expect(1).toEqual(1);
  });

  it("should pass equal strings", function() {
    expect("testing").toEqual("testing");
  });

  it("should pass equal booleans", function() {
    expect(true).toEqual(true);
  });

  it("should pass equal objects", function() {
    expect({a: "testing"}).toEqual({a: "testing"});
  });

  it("should pass equal arrays", function() {
    expect([1, 2, 3]).toEqual([1, 2, 3]);
  });
});

The toBe built-in matcher

The toBe matcher has a very similar behavior to the toEqual matcher, in fact it gives the same result while comparing primitive values, but the similarities stop there.

While the toEqual matcher has a complex implementation (you should take a look at the Jasmine source code) that checks if all attributes of an object and all elements of an array are the same, here it is a simple use of the strict equals operator (===).

If you are unfamiliar with the strict equals operator, its main difference from the equals operator (==) is that the latter performs type coercion if the compared values aren't of the same type.

Tip

The strict equals operator always considers false any comparison between values of distinct types.

Here are some examples of how this matcher (and the strict equals operator) works:

describe("toBe", function() {
  it("should pass equal numbers", function() {
    expect(1).toBe(1);
  });

  it("should pass equal strings", function() {
    expect("testing").toBe("testing");
  });

  it("should pass equal booleans", function() {
    expect(true).toBe(true);
  });

  it("should pass same objects", function() {
    var object = {a: "testing"};
    expect(object).toBe(object);
  });

  it("should pass same arrays", function() {
    var array = [1, 2, 3];
    expect(array).toBe(array);
  });

  it("should not pass equal objects", function() {
    expect({a: "testing"}).not.toBe({a: "testing"});
  });

  it("should not pass equal arrays", function() {
    expect([1, 2, 3]).not.toBe([1, 2, 3]);
  });
});

It is advised to use the toEqual operator in most cases, and resort to the toBe matcher only when you want to check if two variables reference the same object.

The toBeTruthy and toBeFalsy matchers

Besides its primitive Boolean type, everything else in the JavaScript language also has an inherent Boolean value, which is generally known as being either truthy or falsy.

Luckily in JavaScript, there are only a few values that are identified as falsy, as shown in the following examples for the toBeFalsy matcher.

describe("toBeFalsy", function () {
  it("should pass undefined", function() {
    expect(undefined).toBeFalsy();
  });

  it("should pass null", function() {
    expect(null).toBeFalsy();
  });

  it("should pass NaN", function() {
    expect(NaN).toBeFalsy();
  });

  it("should pass the false boolean value", function() {
    expect(false).toBeFalsy();
  });

  it("should pass the number 0", function() {
    expect(0).toBeFalsy();
  });

  it("should pass an empty string", function() {
    expect("").toBeFalsy();
  });
});

Everything else is considered truthy, as demonstrated by these examples of the toBeTruthy matcher:

describe("toBeTruthy", function() {
  it("should pass the true boolean value", function() {
    expect(true).toBeTruthy();
  });

  it("should pass any number different than 0", function() {
    expect(1).toBeTruthy();
  });
  it("should pass any non empty string", function() {
    expect("a").toBeTruthy();
  });

  it("should pass any object (including an array)", function() {
    expect([]).toBeTruthy();
    expect({}).toBeTruthy();
  });
});

But if you want to check if something is equal to an actual Boolean value, it might be a better idea to use the toEqual matcher.

The toBeUndefined, toBeNull, and toBeNaN built-in matchers

These matchers are pretty straightforward, and should be used to check for undefined, null, and NaN values.

describe("toBeNull", function() {
  it("should pass null", function() {
    expect(null).toBeNull();
  });
});

describe("toBeUndefined", function() {
  it("should pass undefined", function() {
    expect(undefined).toBeUndefined();
  });
});

describe("toBeNaN", function() {
  it("should pass NaN", function() {
    expect(NaN).toBeNaN();
  });
});

Both toBeNull and toBeUndefined can be written as toBe(null) and toBe(undefined) respectively, but that is not the case with toBeNaN.

In JavaScript, the NaN value is not equal to any value, not even NaN. So trying to compare it to itself is always false.

NaN === NaN // false

The Jasmine toBeNaN matcher is actually implemented taking into account that NaN is the only value that is not equal to itself. Here is how it is implemented:

jasmine.Matchers.prototype.toBeNaN = function() {
  return (this.actual !== this.actual);
};

See how it checks if the actual is different from itself? So as a good practice, try to use these matchers whenever possible instead of their toBe counterparts.

The toBeDefined built-in matcher

This matcher is useful if you want to check whether a variable is defined and you don't care about its value.

describe("toBeDefined", function() {
  it("should pass any value other than undefined", function() {
    expect(null).toBeDefined();
  });
});

Anything except undefined will pass under this matcher, even null.

The toContain built-in matcher

Sometimes it is desirable to check if an array contains an element or if a string can be found inside another string. For these use cases, you can use the toContain matcher.

describe("toContain", function() {
  it("should pass if a string contains another string", function(){
    expect("My big string").toContain("big");
  });

  it("should pass if an array contains an element", function() {
    expect([1, 2, 3]).toContain(2);
  });
});

The toMatch built-in matcher

Although the toContain and the toEqual matchers can be used in most string comparisons, sometimes the only way to assert if a string value is correct, is through a regular expression. For these cases, you can use the toMatch matcher along with a regular expression.

describe("toMatch", function() {
  it("should pass a matching string", function() {
    expect("My big matched string").toMatch(/My(.+)string/);
  });
});

The matcher works by testing the actual value ("My big matched string") against the expected regular expression (/My(.+)string/).

The toBeLessThan and toBeGreaterThan built-in matchers

The toBeLessThan and toBeGreaterThan are two simple matchers to perform numeric comparisons, as best described by the following examples:

  describe("toBeLessThan", function() {
    it("should pass when the actual is less than expected",function() {
      expect(1).toBeLessThan(2);
    });
  });

  describe("toBeGreaterThan", function() {
    it("should pass when the actual is greater than expected",function() {
      expect(2).toBeGreaterThan(1);
    });
  });

The toThrow built-in matcher

Exceptions are a language's way to demonstrate when something goes wrong.

So for example while coding an API, you might decide to throw an exception when a parameter is passed incorrectly. So how do you test this code?

Jasmine has the built-in toThrow matcher that can be used to verify that an exception has been thrown.

The way it works is a little bit different from the other matchers. Since the matcher has to run a piece of code and check if it throws an exception, the matcher's actual value must be a function.

Here is an example of how it works:

describe("toThrow", function() {
  it("should pass when the exception is thrown", function() {
    expect(function () {
      throw("Some exception");
    }).toThrow("Some exception");
  });
});

When the test is run, the anonymous function gets executed, and if it throws the Some exception exception, the test passes.

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

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