Nested describes

Nested describes are useful when you want to describe similar behavior between specs. Suppose we want the following two new acceptance criteria:

  • Given an investment, when its stock share price valorizes, it should have a positive return on investment (ROI)
  • Given an investment, when its stock share price valorizes, it should be a good investment

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).

Setup and teardown

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:

  • Jasmine runs all setup functions (beforeEach) from the outside in
  • Jasmine runs a spec code (it)
  • Jasmine runs all the teardown functions (afterEach) from the inside out

So, 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;
    });
  });
});

Coding a spec with shared behavior

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:

Coding a spec with shared behavior

This shows the nested describe specs pass

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 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.

Custom matchers

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:

Custom matchers

This is the custom matcher's message

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:

Custom matchers

This is the 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 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:

Custom matchers

This is the custom matcher's wrong custom negated message

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:

Custom matchers

This shows the custom matcher's custom dynamic message

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.

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 of how JavaScript handles type.

The toEqual built-in matcher

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 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 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.

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 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.

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 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.

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, as shown in the following code:

NaN === NaN // false

As good practice, try to use these matchers instead of their toBe counterparts whenever possible.

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, 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.

The toContain built-in matcher

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);
  });
});

The toMatch built-in matcher

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 built-in matchers

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);
    });
  });

The toBeCloseTo built-in matcher

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.

The toThrow built-in matcher

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.

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

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