Nested describes are useful when you want to describe similar behavior between specs. Suppose we want the following two new acceptance criteria:
Both these criteria share the same behavior when the investment's stock share price valorizes.
To translate this into Jasmine, you can nest a call to the describe
function inside the existing one in the InvestmentSpec.js
file (I removed the rest of the code for the purpose of demonstration; it is still there):
describe("Investment", function() describe("when its stock share price valorizes", function() { }); });
It should behave just like the outer one, so you can add specs (it
) and use the setup and teardown functions (beforeEach
, afterEach
).
When using the setup and teardown functions, Jasmine respects the outer setup and teardown functions as well, so that they are run as expected. For each spec (it
), the following actions are performed:
beforeEach
) from the outside init
)afterEach
) from the inside outSo, we can add a setup function to this new describe
function that changes the share price of the stock, so that it's greater than the share price of the investment:
describe("Investment", function() { var stock; var investment; beforeEach(function() { stock = new Stock(); investment = new Investment({ stock: stock, shares: 100, sharePrice: 20 }); }); describe("when its stock share price valorizes", function() { beforeEach(function() { stock.sharePrice = 40; }); }); });
Now that we have the shared behavior implemented, we can start coding the acceptance criteria described earlier. Each is, just as before, a call to the global Jasmine function it
:
describe("Investment", function() { describe("when its stock share price valorizes", function() { beforeEach(function() { stock.sharePrice = 40; }); it("should have a positive return of investment", function() { expect(investment.roi()).toEqual(1); }); it("should be a good investment", function() { expect(investment.isGood()).toEqual(true); }); }); });
After adding the missing functions to Investment
in the Investment.js
file:
Investment.prototype.roi = function() { return (this.stock.sharePrice - this.sharePrice) / this.sharePrice; }; Investment.prototype.isGood = function() { return this.roi() > 0; };
You can run the specs and see that they pass:
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
and toEqual
matchers. These are the two base 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.
Consider this expectation from the previous section:
expect(investment.isGood()).toEqual(true);
Although it works, it is not very expressive. Imagine if we could instead rewrite it as:
expect(investment).toBeAGoodInvestment();
This creates a much better relation with the acceptance criterion:
So, here "should be a good investment" becomes "expect investment to be a good investment".
Implementing it is quite simple. You do so by calling the jasmine.addMatchers
function—ideally inside a setup step (beforeEach
).
Although you can put this new matcher definition inside the InvestmentSpec.js
file, Jasmine already has a default place to add custom matchers, the SpecHelper.js
file, inside the spec
folder. If you are using Standalone Distribution, it already comes with a sample custom matcher; delete it and let's start from scratch.
The addMatchers
function accepts a single parameter—an object where each attribute corresponds to a new matcher. So, to add the following new matcher, change the contents of the SpecHelper.js
file to the following:
beforeEach(function() {
jasmine.addMatchers({
toBeAGoodInvestment: function() {}
});
});
The function being defined here is not the matcher itself but a factory function to build the matcher. Its purpose, once called is to return an object containing a compare function, as follows:
jasmine.addMatchers({ toBeAGoodInvestment: function () { return { compare: function (actual, expected) { // matcher definition } }; } });
The compare
function will contain the actual matcher implementation, and as can be observed by its signature, it receives both values being compared (the actual
and expected
values).
For the given example, the investment
object will be available in the actual
argument.
Then, Jasmine expects, as the result of this compare
function, an object with a pass
attribute with a boolean value true
to indicate that the expectation passes and false
if the expectation fails.
Let's have a look at the following valid implementation of the toBeAGoodInvestment
matcher:
toBeAGoodInvestment: function () { return { compare: function (actual, expected) { var result = {}; result.pass = actual.isGood(); return result; } }; }
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 pass. 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 the Investment.js
file, in the src
folder to always return false
:
Investment.prototype.isGood = function() {
return false;
};
When running the specs again, Jasmine generates an error message stating Expected { stock: { sharePrice: 40 }, shares: 100, sharePrice: 20, cost: 2000 } to be a good investment
, as shown in the following screenshot:
Jasmine does a great job generating this error message, but it also allows its customization via the result.message
property of the object returned as the result of the matcher. Jasmine expects this property to be a string with the following error message:
toBeAGoodInvestment: function () {
return {
compare: function (actual, expected) {
var result = {};
result.pass = actual.isGood();
result.message = 'Expected investment to be a good investment';
return result;
}
};
}
Run the specs again and the error message should change:
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 the InvestmentSpec.js
file inside the spec
folder by adding new and nested describe
and spec
, as follows:
describe("when its stock share price devalorizes", function() { beforeEach(function() { stock.sharePrice = 0; }); it("should have a negative return of investment", function() { expect(investment.roi()).toEqual(-1); }); it("should be a bad investment", function() { expect(investment).not.toBeAGoodInvestment(); }); });
But there is a catch! Let's break the investment
implementation in the Investment.js
file code so that it is always a good investment, as follows:
Investment.prototype.isGood = function() {
return true;
};
After running the specs again, you can see that this new spec fails, but the error message, Expected investment to be a good investment
, is wrong, as shown in the following screenshot:
That is the message that was hardcoded inside the matcher. To fix this, you need to make the message dynamic.
Jasmine only shows the message if the matcher fails, so the proper way of making this message dynamic is to consider what message is supposed to be shown when the given comparison is invalid:
compare: function (actual, expected) { var result = {}; result.pass = actual.isGood(); if (actual.isGood()) { result.message = 'Expected investment to be a bad investment'; } else { result.message = 'Expected investment to be a good investment'; } return result; }
This fixes the message, as shown in the following screenshot:
Now this matcher can be used anywhere.
Before continuing in the chapter, change the isGood
method back again to its correct implementation:
Investment.prototype.isGood = function() { return this.roi() > 0; };
What this example lacked was a way to show how to pass an expected value to a matcher like this:
expect(investment.cost).toBe(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 in the SpecHelper.js
file, inside the spec
folder, as follows:
beforeEach(function() { jasmine.addMatchers({ toBe: function () { return { compare: function (actual, expected) { return actual === expected; } }; } }); });
By implementing any matcher, check first whether there is one available that already does what you want.
For more information, check the official documentation at the Jasmine website http://jasmine.github.io/2.1/custom_matcher.html.
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 of how JavaScript handles type.
The toEqual
matcher 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), as shown in the following code:
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
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 whether 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.
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 that you use the toEqual
operator in most cases and resort to the toBe
matcher only when you want to check whether two variables reference the same object.
Besides its primitive boolean type, everything else in the JavaScript language also has an inherent boolean value, which is generally known to be 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 the following 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 whether something is equal to an actual boolean value, it might be a better idea to use the toEqual
matcher.
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
, as shown in the following code:
NaN === NaN // false
As good practice, try to use these matchers instead of their toBe
counterparts whenever possible.
This matcher is useful if you want to check whether a variable is defined and you don't care about its value, as follows:
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
.
Sometimes, it is desirable to check whether an array contains an element, or whether a string can be found inside another string. For these use cases, you can use the toContain
matcher, as follows:
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); }); });
Although the toContain
and toEqual
matchers can be used in most string comparisons, sometimes the only way to assert whether a string value is correct is through a regular expression. For these cases, you can use the toMatch
matcher along with a regular expression, as follows:
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
matchers are simple and used to perform numeric comparisons—something that is 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); }); });
This is a special matcher used to compare floating-point numbers with a defined set of precision—something that is best explained by this example:
describe("toBeCloseTo", function() { it("should pass when the actual is closer with a given precision", function() { expect(3.1415).toBeCloseTo(2.8, 0); expect(3.1415).not.toBeCloseTo(2.8, 1); }); });
The first parameter is the number being compared, and the second is the precision in the number of decimal cases.
Exceptions are a language's way of demonstrating 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 whether 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 is executed, and if it throws the Some exception
exception, the test passes.