Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

Designing Jasmine Tests with Spies

Save for later
  • 17 min read
  • 22 Apr 2015

article-image

In this article by Munish Sethi, author of the book Jasmine Cookbook, we will see the implementation of Jasmine tests using spies.

(For more resources related to this topic, see here.)

Nowadays, JavaScript has become the de facto programming language to build and empower frontend/web applications. We can use JavaScript to develop simple or complex applications. However, applications in production are often vulnerable to bugs caused by design inconsistencies, logical implementation errors, and similar issues. Due to this, it is usually difficult to predict how applications will behave in real-time environments, which leads to unexpected behavior, nonavailability of applications, or outage for shorter/longer durations. This generates lack of confidence and dissatisfaction among application users. Also, high cost is often associated with fixing the production bugs. Therefore, there is a need to develop applications with high quality and high availability.

Jasmine is a Behavior-Driven development (BDD) framework for testing JavaScript code both in browser and on server side. It plays a vital role to establish effective development process by applying efficient testing processes. Jasmine provides a rich set of libraries to design and develop Jasmine specs (unit tests) for JavaScript (or JavaScript enabled) applications.

In this article, we will see how to develop specs using Jasmine spies and matchers. We will also see how to write Jasmine specs with the Data-Driven approach using JSON/HTML fixture from end-to-end (E2E) perspective.

Let's understand the concept of mocks before we start developing Jasmine specs with spies.

Generally, we write one unit test corresponding to a Jasmine spec to test a method, object, or component in isolation, and see how it behaves in different circumstances. However, there are situations where a method/object has dependencies on other methods or objects. In this scenario, we need to design tests/specs across the unit/methods or components to validate behavior or simulate a real-time scenario. However, due to nonavailability of dependent methods/objects or staging/production environment, it is quite challenging to write Jasmine tests for methods that have dependencies on other methods/objects. This is where Mocks come into the picture. A mock is a fake object that replaces the original object/method and imitates the behavior of the real object without going into the nitty-gritty or creating the real object/method.

Mocks work by implementing the proxy model. Whenever, we create a mock object, it creates a proxy object, which replaces the real object/method. We can then define the methods that are called and their returned values in our test method. Mocks can then be utilized to retrieve some of the runtime statistics, as follows:

  • How many times was the mocked function/object method called?
  • What was the value that the function returned to the caller?
  • With how many arguments was the function called?

Developing Jasmine specs using spies

In Jasmine, mocks are referred to as spies. Spies are used to mock a function/object method. A spy can stub any function and track calls to it and all its arguments. Jasmine provides a rich set of functions and properties to enable mocking. There are special matchers to interact with spies, that is, toHaveBeenCalled and toHaveBeenCalledWith.

Now, to understand the preceding concepts, let's assume that you are developing an application for a company providing solutions for the healthcare industry. Currently, there is a need to design a component that gets a person's details (such as name, age, blood group, details of diseases, and so on) and processes it further for other usage. Now, assume that you are developing a component that verifies a person's details for blood or organ donation. There are also a few factors or biological rules that exist to donate or receive blood. For now, we can consider the following biological factors:

  • The person's age should be greater than or equal to 18 years
  • The person should not be infected with HIV+

Let's create the validate_person_eligibility.js file and consider the following code in the current context:

var Person = function(name, DOB, bloodgroup, donor_receiver) {
   this.myName = name;
this.myDOB = DOB;
this.myBloodGroup = bloodgroup;
this.donor_receiver = donor_receiver;
this.ValidateAge    = function(myDOB){
    this.myDOB = myDOB || DOB;
    return this.getAge(this.myDOB);
   };
   this.ValidateHIV   = function(personName,personDOB,personBloodGroup){
    this.myName = personName || this.myName;
    this.myDOB = personDOB || this.myDOB;
    this.myBloodGroup = personBloodGroup || this.myBloodGroup;
    return this.checkHIV(this.myName, this.myDOB, this.myBloodGroup);
   };
};
Person.prototype.getAge = function(birth){
console.log("getAge() function is called");
var calculatedAge=0;
// Logic to calculate person's age will be implemented later
 
if (calculatedAge<18) {
   throw new ValidationError("Person must be 18 years or older");
};
return calculatedAge;
};
Person.prototype.checkHIV = function(pName, pDOB, pBloodGroup){
console.log("checkHIV() function is called");
bolHIVResult=true;
// Logic to verify HIV+ will be implemented later
 
if (bolHIVResult == true) {
   throw new ValidationError("A person is infected with HIV+");  
};
return bolHIVResult;
};
 
// Define custom error for validation
function ValidationError(message) {
this.message = message;
}
ValidationError.prototype = Object.create(Error.prototype);

In the preceding code snapshot, we created an object Person, which accepts four parameters, that is, name of the person, date of birth, the person's blood group, and the donor or receiver. Further, we defined the following functions within the person's object to validate biological factors:

  • ValidateAge(): This function accepts an argument as the date of birth and returns the person's age by calling the getAge function. You can also notice that under the getAge function, the code is not developed to calculate the person's age.
  • ValidateHIV(): This function accepts three arguments as name, date of birth, and the person's blood group. It verifies whether the person is infected with HIV or not by calling the checkHIV function. Under the function checkHIV, you can observe that code is not developed to check whether the person is infected with HIV+ or not.

Next, let's create the spec file (validate_person_eligibility_spec.js) and code the following lines to develop the Jasmine spec, which validates all the test conditions (biological rules) described in the previous sections:

describe("<ABC> Company: Health Care Solution, ", function() {
describe("When to donate or receive blood, ", function(){
   it("Person's age should be greater than " +
       "or equal to 18 years", function() {
     var testPersonCriteria = new Person();
     spyOn(testPersonCriteria, "getAge");
     testPersonCriteria.ValidateAge("10/25/1990");
     expect(testPersonCriteria.getAge).toHaveBeenCalled();
     expect(testPersonCriteria.getAge).toHaveBeenCalledWith("10/25/1990");
   });
   it("A person should not be " +
       "infected with HIV+", function() {
     var testPersonCriteria = new Person();
     spyOn(testPersonCriteria, "checkHIV");
     testPersonCriteria.ValidateHIV();
     expect(testPersonCriteria.checkHIV).toHaveBeenCalled();
   });
});
});

In the preceding snapshot, we mocked the functions getAge and checkHIV using spyOn(). Also, we applied the toHaveBeenCalled matcher to verify whether the function getAge is called or not.

Let's look at the following pointers before we jump to the next step:

  • Jasmine provides the spyOn() function to mock any JavaScript function. A spy can stub any function and track calls to it and to all arguments. A spy only exists in the describe or it block; it is defined, and will be removed after each spec.
  • Jasmine provides special matchers, toHaveBeenCalled and toHaveBeenCalledWith, to interact with spies. The matcher toHaveBeenCalled returns true if the spy was called. The matcher toHaveBeenCalledWith returns true if the argument list matches any of the recorded calls to the spy.

Let's add the reference of the validate_person_eligibility.js file to the Jasmine runner (that is, SpecRunner.html) and run the spec file to execute both the specs. You will see that both the specs are passing, as shown in the following screenshot:

designing-jasmine-tests-spies-img-0

While executing the Jasmine specs, you can notice that log messages, which we defined under the getAge() and checkHIV functions, are not printed in the browser console window.

Whenever, we mock a function using Jasmine's spyOn() function, it replaces the original method of the object with a proxy method.

Next, let's consider a situation where the function <B> is called under the function <A>, which is mocked in your test. Due to the mock behavior, it creates a proxy object that replaces the function <A>, and function <B> will never be called. However, in order to pass the test, it needs to be executed.

In this situation, we chain the spyOn() function with .and.callThrough. Let's consider the following test code:

it("Person's age should be greater than " +
   "or equal to 18 years", function() {
var testPersonCriteria = new Person();
 spyOn(testPersonCriteria, "getAge").and.callThrough();
testPersonCriteria.ValidateAge("10/25/1990");
expect(testPersonCriteria.getAge).toHaveBeenCalled();
expect(testPersonCriteria.getAge).toHaveBeenCalledWith("10/25/1990");
});

Whenever the spyOn() function is chained with and.callThrough, the spy will still track all calls to it. However, in addition, it will delegate the control back to the actual implementation/function.

To see the effect, let's run the spec file check_person_eligibility_spec.js with the Jasmine runner. You will see that the spec is failing, as shown in the following screenshot:

designing-jasmine-tests-spies-img-1

This time, while executing the spec file, you can notice that a log message (that is, getAge() function is called) is also printed in the browser console window.

On the other hand, you can also define your own logic or set values in your test code as per specific requirements by chaining the spyOn() function with and.callFake. For example, consider the following code:

it("Person's age should be greater than " +
   "or equal to 18 years", function() {
var testPersonCriteria = new Person();
 spyOn(testPersonCriteria, "getAge").and.callFake(function() 
     {
    return 18;
 });
testPersonCriteria.ValidateAge("10/25/1990");
expect(testPersonCriteria.getAge).toHaveBeenCalled();
expect(testPersonCriteria.getAge).toHaveBeenCalledWith("10/25/1990");
 expect(testPersonCriteria.getAge()).toEqual(18);
});

Whenever the spyOn() function is chained with and.callFake, all calls to the spy will be delegated to the supplied function.

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at ₹800/month. Cancel anytime

You can also notice that we added one more expectation to validate the person's age.

To see execution results, run the spec file with the Jasmine runner. You will see that both the specs are passing:

designing-jasmine-tests-spies-img-2

Implementing Jasmine specs using custom spy method

In the previous section, we looked at how we can spy on a function. Now, we will understand the need of custom spy method and how Jasmine specs can be designed using it.

There are several cases when one would need to replace the original method. For example, original function/method takes a long time to execute or it depends on the other method/object (or third-party system) that is/are not available in the test environment. In this situation, it is beneficial to replace the original method with a fake/custom spy method for testing purpose. Jasmine provides a method called jasmine.createSpy to create your own custom spy method.

As we described in the previous section, there are few factors or biological rules that exist to donate or receive bloods. Let's consider few more biological rules as follows:

  • Person with O+ blood group can receive blood from a person with O+ blood group
  • Person with O+ blood group can give the blood to a person with A+ blood group

First, let's update the JavaScript file validate_person_eligibility.js and add a new method ValidateBloodGroup to the Person object. Consider the following code:

this.ValidateBloodGroup   = function(callback){
var _this = this;
var matchBloodGroup;
this.MatchBloodGroupToGiveReceive(function (personBloodGroup) {
   _this.personBloodGroup = personBloodGroup;
   matchBloodGroup = personBloodGroup;
   callback.call(_this, _this.personBloodGroup);
});
return matchBloodGroup;
};  
Person.prototype.MatchBloodGroupToGiveReceive = function(callback){
// Network actions are required to match the values corresponding
// to match blood group. Network actions are asynchronous hence the
// need for a callback.
// But, for now, let's use hard coded values.
var matchBloodGroup;
if (this.donor_receiver == null || this.donor_receiver == undefined){
   throw new ValidationError("Argument (donor_receiver) is missing ");
};
if (this.myBloodGroup == "O+" && this.donor_receiver.toUpperCase() == "RECEIVER"){
   matchBloodGroup = ["O+"];
}else if (this.myBloodGroup == "O+" && this.donor_receiver.toUpperCase() == "DONOR"){
   matchBloodGroup = ["A+"];
};
callback.call(this, matchBloodGroup);
};

In the preceding code snapshot, you can notice that the ValidateBloodGroup() function accepts an argument as the callback function. The ValidateBloodGroup() function returns matching/eligible blood group(s) for receiver/donor by calling the MatchBloodGroupToGiveReceive function.

Let's create the Jasmine tests with custom spy method using the following code:

describe("Person With O+ Blood Group: ", function(){
it("can receive the blood of the " +
     "person with O+ blood group", function() {
   var testPersonCriteria = new Person("John Player", "10/30/1980", "O+", "Receiver");
   spyOn(testPersonCriteria, "MatchBloodGroupToGiveReceive").and.callThrough(); 
   var callback = jasmine.createSpy();
   testPersonCriteria.ValidateBloodGroup(callback);
   //Verify, callback method is called or not
   expect(callback).toHaveBeenCalled();
   //Verify, MatchBloodGroupToGiveReceive is
   // called and check whether control goes back
   // to the function or not
   expect(testPersonCriteria.MatchBloodGroupToGiveReceive).toHaveBeenCalled();
   expect(testPersonCriteria.MatchBloodGroupToGiveReceive.calls.any()).toEqual(true);     
   expect(testPersonCriteria.MatchBloodGroupToGiveReceive.calls.count()).toEqual(1);
   expect(testPersonCriteria.ValidateBloodGroup(callback)).toContain("O+");
});
it("can give the blood to the " +
     "person with A+ blood group", function() {
   var testPersonCriteria = new Person("John Player", "10/30/1980", "O+", "Donor");
   spyOn(testPersonCriteria, "MatchBloodGroupToGiveReceive").and.callThrough();
   var callback = jasmine.createSpy();
   testPersonCriteria.ValidateBloodGroup(callback);
   expect(callback).toHaveBeenCalled();
   expect(testPersonCriteria.MatchBloodGroupToGiveReceive).toHaveBeenCalled();
   expect(testPersonCriteria.ValidateBloodGroup(callback)).toContain("A+");
});
});

You can notice that in the preceding snapshot, first we mocked the function MatchBloodGroupToGiveReceive using spyOn() and chained it with and.callThrough() to hand over the control back to the function. Thereafter, we created callback as the custom spy method using jasmine.createSpy. Furthermore, we are tracking calls/arguments to the callback and MatchBloodGroupToGiveReceive functions using tracking properties (that is, .calls.any() and .calls.count()).

Whenever we create a custom spy method using jasmine.createSpy, it creates a bare spy. It is a good mechanism to test the callbacks. You can also track calls and arguments corresponding to custom spy method. However, there is no implementation behind it.

To execute the tests, run the spec file with the Jasmine runner. You will see that all the specs are passing:

designing-jasmine-tests-spies-img-3

Implementing Jasmine specs using Data-Driven approach

In Data-Driven approach, Jasmine specs get input or expected values from the external data files (JSON, CSV, TXT files, and so on), which are required to run/execute tests. In other words, we isolate test data and Jasmine specs so that one can prepare the test data (input/expected values) separately as per the need of specs. For example, in the previous section, we provided all the input values (that is, name of person, date of birth, blood group, donor or receiver) to the person's object in the test code itself. However, for better management, it's always good to maintain test data and code/specs separately.

To implement Jasmine tests with the data-driven approach, let's create a data file fixture_input_data.json. For now, you can use the following data in JSON format:

[
{
"Name": "John Player",
"DOB": "10/30/1980",
"Blood_Group": "O+",
"Donor_Receiver": "Receiver"
},
{
"Name": "John Player",
"DOB": "10/30/1980",
"Blood_Group": "O+",
"Donor_Receiver": "Donor"
}
]

Next, we will see how to provide all the required input values in our tests through a data file using the jasmine-jquery plugin. Before we move to the next step and implement the Jasmine tests with the Data-Driven approach, let's note the following points regarding the jasmine-jquery plugin:

  • It provides two extensions to write the tests with HTML and JSON fixture:
    • An API for handling HTML and JSON fixtures in your specs
    • A set of custom matchers for jQuery framework
  • The loadJSONFixtures method loads fixture(s) from one or more JSON files and makes it available at runtime

To know more about the jasmine-jquery plugin, you can visit the following website:

https://github.com/velesin/jasmine-jquery

Let's implement both the specs created in the previous section using the Data-Driven approach. Consider the following code:

describe("Person With O+ Blood Group: ", function(){
   var fixturefile, fixtures, myResult;
beforeEach(function() {
       //Start - Load JSON Files to provide input data for all the test scenarios
       fixturefile = "fixture_input_data.json";
       fixtures = loadJSONFixtures(fixturefile);
       myResult = fixtures[fixturefile];     
       //End - Load JSON Files to provide input data for all the test scenarios
});  
it("can receive the blood of the " +
     "person with O+ blood group", function() {
   //Start - Provide input values from the data file
   var testPersonCriteria = new Person(
       myResult[0].Name,
       myResult[0].DOB,
       myResult[0].Blood_Group,
       myResult[0].Donor_Receiver
   );
   //End - Provide input values from the data file
   spyOn(testPersonCriteria, "MatchBloodGroupToGiveReceive").and.callThrough();
   var callback = jasmine.createSpy();
   testPersonCriteria.ValidateBloodGroup(callback);
   //Verify, callback method is called or not
   expect(callback).toHaveBeenCalled();
   //Verify, MatchBloodGroupToGiveReceive is
   // called and check whether control goes back
   // to the function or not
   expect(testPersonCriteria.MatchBloodGroupToGiveReceive).toHaveBeenCalled();
   expect(testPersonCriteria.MatchBloodGroupToGiveReceive.calls.any()).toEqual(true);    
   expect(testPersonCriteria.MatchBloodGroupToGiveReceive.calls.count()).toEqual(1);
   expect(testPersonCriteria.ValidateBloodGroup(callback)).toContain("O+");
});
it("can give the blood to the " +
     "person with A+ blood group", function() {
   //Start - Provide input values from the data file
   var testPersonCriteria = new Person(
       myResult[1].Name,
       myResult[1].DOB,
       myResult[1].Blood_Group,
       myResult[1].Donor_Receiver
   );
   //End - Provide input values from the data file
   spyOn(testPersonCriteria, "MatchBloodGroupToGiveReceive").and.callThrough();
   var callback = jasmine.createSpy();
   testPersonCriteria.ValidateBloodGroup(callback);
   expect(callback).toHaveBeenCalled();
   expect(testPersonCriteria.MatchBloodGroupToGiveReceive).toHaveBeenCalled();
   expect(testPersonCriteria.ValidateBloodGroup(callback)).toContain("A+");
});
});

In the preceding code snapshot, you can notice that first we provided the input data from an external JSON file (that is, fixture_input_data.json) using the loadJSONFixtures function and made it available at runtime. Thereafter, we provided input values/data to both the specs, as required; we set the value of name, date of birth, blood group, and donor/receiver for specs 1 and 2, respectively.

Further, following the same methodology, we can also create a separate data file for expected values, which we require in our tests to compare with actual values.

If test data (input or expected values) is required during execution, it is advisable to provide it from an external file instead of using hardcoded values in your tests.

Now, execute the test suite with the Jasmine runner and you will see that all the specs are passing:

designing-jasmine-tests-spies-img-4

Summary

In this article, we looked at the implementation of Jasmine tests using spies. We also demonstrated how to test the callback function using custom spy method. Further, we saw the implementation of Data-Driven approach, where you learned how to to isolate test data from the code.

Resources for Article:


Further resources on this subject: