Notice

╠ This is my personal blog and my posts here have nothing to do with my employers or any other association I may have. It is my personal blog for my personal experience, ideas and notes. ╣

Sunday, February 5, 2017

Spying on JavaScript Method using Jasmine [Part - 2]

Basic Introduction of JavaScript Testing Framework Jasmine

The basic of Jasmine is already discussed in my earlier blog. To read it please click here.


Spying with Jasmine

One of the basic reason of spying is to do unit testing of method in isolation. So we mock the method to bypass other dependencies.  

Let's see what Jasmine offer us.

Spy on a method 

spyOn(object, methodName) it can only exist inside the describe or it block. In another words it can only be define inside spec or suit, and it will be removed once spec get existed.  

spyOn(obj , 'setName');

When you want to invoke the actual method but call will be tracked.

spyOn(obj , 'setName').and.callThrough();

When you call and.callThrough, the spy acts as a proxy, calling the real function, but passing through a spy object allowing you to add tests like expectation.

When you want to remove the effect of spyOn(obj , 'setName').and.callThrough()

spyOn(obj , 'setName').and.stub() removes the effect of
spyOn(obj , 'setName').and.callThrough() on a spy.

When you want to return a specific value on all calls.

spyOn(obj, "getName").and.returnValue('Andy');

When you want to invoke a custom function.

spyOn(obj, "getName").and.callFake(function(arguments, here) { return 'Andy'; });

When you want to throw specific error.

spyOn(obj, "setName").and.throwError("myError");




Every call to a spy is tracked and exposed on the calls property.


Calls Purpose
any() returns false if the spy has not been called at all, and then true once at least one call happens.
expect(obj.setName.calls.any()).toEqual('Andy');
count() returns the number of times the spy was called.
expect(obj.setName.calls.count()).toEqual(2);
argsFor(index) returns the arguments passed to call number index.
obj.setName('Andy'); expect(obj.setName.calls.argsFor(0)).toEqual(["Andy"]);
allArgs() returns the arguments to all calls.
obj.setName('Andy'); 
obj.setName('Anindya','Banerjee'); 
expect(obj.setName.calls.allArgs()).toEqual([["Andy"],["Anindya","Banerjee"]]);
all() returns the context (the this) and arguments passed all calls.
obj.setName('Andy'); 
expect(obj.setName.calls.all()).toEqual({object: obj, args: ["Andy"]});
mostRecent() returns the context (the this) and arguments for the most recent call.
obj.setName('Andy'); 
obj.setName('Anindya','Banerjee'); 
expect(obj.setName.calls.mostRecent()).toEqual({object: obj, args:"Anindya","Banerjee"});
first() returns the context (the this) and arguments for the first call.
obj.setName('Andy'); 
obj.setName('Anindya','Banerjee');
expect(obj.setName.calls.first()).toEqual({object: obj, args:"Andy"});

reset()
clears all tracking for a spy.
obj.setName.calls.reset();

There is a property named object which actually return the this object when the spy was call. This can be used with all/mostReset/first.
Please note here these methods are return an array of json object and one of key is object. Please refer to the above table.

Here is an example.

obj.setName('Andy');
obj.setName('Anindya','Banerjee');
expect(obj.setName.calls.all().object).toEqual(obj);
expect(obj.setName.calls.mostRecent().object).toEqual(obj);
expect(obj.setName.calls.first().object).toEqual(obj);



There are three matchers toHaveBeenCalledtoHaveBeenCalledWith and toHaveBeenCalledTimes.

toHaveBeenCalled return true if the spy was called.

expect(obj.setName).toHaveBeenCalled();


toHaveBeenCalledWith return true if the argument list matches any of the recorded calls to the spy.


expect(obj.setName).toHaveBeenCalledWith('Andy');

toHaveBeenCalledTimes return true if the spy was called the specified number of times.

expect(obj.setName).toHaveBeenCalledTimes(2);


What we will do if we didn't have a function to spy on ?

Jasmine provide functions to deal with this kind of situations. 

Function Purpose
jasmine.createSpy create a spy function which doesn't exist.
var dummyFunction = jasmine.createSpy('dummy function'); $('#mybutton').click(dummyFunction);
jasmine.createSpyObj create multiple spy function which doesn't exist.
// Suppose you got a Json object from back-end // Person is a spy object with getName and getAge methods and an id property var person = jasmine.createSpyObj("person", ["getName", "getAge"]); person.id = 1234;
jasmine.any returns true if the constructor matches the constructor of the actual value.
expect({}).toEqual(jasmine.any(Object)); expect(12).toEqual(jasmine.any(Number));
jasmine.anything returns true if the actual value is not null or undefined.
expect({}).toEqual(jasmine.anything());
jasmine.objectContaining when an expectation only cares about certain key/value pairs in the actual.
var person = { firstName : "Anindya", lastName : "Banerjee", aliasName : "Andy" }; expect(person).toEqual(jasmine.objectContaining({ aliasName: "Andy" }));
jasmine.arrayContaining when an expectation only cares about some of the values in an array.
var num = [1, 2, 3, 4]; expect(num).toEqual(jasmine.arrayContaining([3, 1])); expect(num).not.toEqual(jasmine.arrayContaining([6]));
jasmine.stringMatching When match a portion of a string in a spy expectation.
expect({names: 'AndyAnindya'}).toEqual({names: jasmine.stringMatching('Andy')});

Basic introduction of JavaScript testing framework Jasmine [Part - 1]

While working on user interface I found that I need to write complex logic in JavaScript
to validate the form data. I started writing validation logic then deploy it in web container and when I find any bug I fix it and redeploy the JavaScript in web container.

Development Cycle



At one point I find that I am simply struggling with complex logic and I realize due to this process I am losing time.

Then I started exploring JavaScript testing framework and asked few of user interface experts they told me that they are using JSUnit for testing but one of them recommend me explore Jasmine JavaScript testing framework. So here I'm to share my experience and a brief tutorial / cheat sheet for JUnit users. 

Jasmine 

Jasmine is a behavior-driven development framework for testing JavaScript code. It does not rely on browsers, DOM, or any JavaScript framework. 

You can download it from here. Here we will be using Jasmine version 2.5.2.

Once you extract the zip we will see three folder, one html file and one license file.
Inside Jasmine zip

  1. /src folder: contains the JavaScript source files that you want to test
  2. /lib folder: contains the framework files
  3. /spec folder: contains the JavaScript testing files
  4. SpecRunner.html : is the test case runner HTML file

Please note that actual JavaScript file need to be included before spec/test JavaScript file.

SpecRunner.html

Learn the Syntax of Jasmine

Suite

A Jasmine suite is a collection of sub-suites and/or test cases to test the behaviour of JavaScript function or JavaScript Object. This suite is define by describe keyword. describe take two parameters, first parameter is string and second parameter is JavaScript function.

describe("Test Suite", function(){
      // test case here
});


An inner test suite example

describe("Test Suite", function(){
   

   describe("1st inner Suite" , function() {

   });

   describe("2nd inner Suite" , function() {

   });
});

To disable a suit just simply put a 'x' before keyword describe.

describe("Test Suite", function(){

   xdescribe("Disable Suite" , function() {


   });
   describe("2nd inner Suite" , function() {





   });
});



Spec

A Jasmine spec represents a test case inside the test suite. This spec is defined by it keyword. it also have two parameters,  first parameter is string and second parameter is JavaScript function.

describe("Test Suite", function(){
      it("Spec", function() {
            // test matchers here
      });
});

To disable a spec just simply put a 'x' before keyword it.

describe("Test Suite", function(){
      xit("Disable Spec", function() {
            // test matchers here
      });
});


Setup and Tear down

For setup and teardown Jasmine provide four global beforeAll, afterAll, beforeEach and afterEach functions.

As these names implied, the beforeAll is invoked once before all the spec in describe are run, and the afterAll function is called after all spec finish and the beforeEach is invoked once before each the spec in describe are run, and the afterEach function is called after each spec finish.

describe("Test Suite", function(){

    beforeAll("Before all spec inside this suite", function() {
         // Suite level setup code here
     });

    beforeEach("Before each spec inside this suite", function() {
         // Spec level setup code here
     });
     
      it("Spec1", function() {
            // test matchers here
      });


      it("Spec2", function() {
            // test matchers here
      }); 

     afterEach("After each spec inside this suite", function() {
          // Spec level tear down code here
     });

     afterAll("After all Spec inside this suite", function() {
          // Suite level tear down code here
     });

});

The this keyword

Another way to share variables between a beforeEach, it, and afterEach is through the this keyword. Each spec's beforeEach/it/afterEach has the this as the same empty object that is set back to empty for the next spec's beforeEach/it/afterEach.


Available Matchers in Jasmine

Matcher Purpose
toBe() passed if the actual value is of the same type and value as that of the expected value. It compares with === operator
toEqual() works for simple literals and variables;
should work for objects too
toMatch() to check whether a value matches a string or a regular expression
toBeCloseTo() check if a number is close to another number, given a certain amount of decimal precision as the second parameter.
expect(12.34).toBeCloseTo(12.3, 1); // success
expect(12.34).toBeCloseTo(12.3, 2); // failure
toBeDefined() to ensure that a property or a value is defined
toBeUndefined() to ensure that a property or a value is undefined
toBeNull() to ensure that a property or a value is null.
toBeNaN() this is different from JavaScript's build-in isNaN function. The build-in will return true for non-numeric type, objects, and arrays. Jasmine's toBeNaN will be positive only if it's the NaN value.
expect(parseInt("hello")).toBeNaN(); // success
toBeTruthy() to ensure that a property or a value is true
toBeFalsy() to ensure that a property or a value is false
toContain() to check whether a string or array contains a substring or an item.
toBeLessThan() for mathematical comparisons of less than
toBeLessThanOrEqual() for mathematical comparisons of less than or equal
toBeGreaterThan() for mathematical comparisons of greater than
toBeGreaterThanOrEqual() for mathematical comparisons of greater than or equal
toBeCloseTo() for precision math comparison
toThrow() for testing if a function throws an exception
toThrowError() for testing aspecificthrown exception
toHaveBeenCalled() return true if the spy was called
toHaveBeenCalledWith() return true if the argument list matches any of the recorded calls to the spy
toHaveBeenCalledTimes() return true if the spy was called the specified number of times



Write and Execute a test

Lets begin with Jasmine , I will be using Eclipse as my intregated development enviroment (IDE) tool. You can use any IDE or notepad editors of your choice.

Step 1

I create a JavaScript project named 'JasminTutorial' and under that project I create a folder named js.
In js folder I copied Jasmine's lib folder and SpecRunner.html file only.

Step 2

Write a JavaScript file which I will be using to test.

HelloWorld.js
  1. HelloWorld = function() {};
  2. HelloWorld.prototype.helloWorld = function(){
  3. return "Hello World!";
  4. }
  5. HelloWorld.prototype.sayHi = function(name) {
  6. return "Hi " + name;
  7. }

Step 3

Now I create a spec folder under js folder to sperate this test file. I create another JavaScript file named HelloWorldSpec.js which is to test HelloWorld.js.


Step 4

Now write the spec.


HelloWorldSpec.js
  1. describe("test HelloWorld", function(){
  2. var helloworld;
  3. beforeAll(function(){
  4. helloworld = new HelloWorld();
  5. });
  6. // 1st Suite for helloWorld
  7. describe("test helloWorld function ", function(){
  8. it("test helloWorld function return 'Hello World!' string", function(){
  9. expect(helloworld.helloWorld()).toEqual("Hello World!");
  10. });
  11. it("do negative test with helloWorld function", function(){
  12. expect(helloworld.helloWorld()).not.toEqual("Hi Andy");
  13. });
  14. });
  15. // 2nd Suite for sayHi
  16. describe("test sayHi function ", function(){
  17. it("test sayHi function return 'Hi Andy' string", function(){
  18. expect(helloworld.sayHi('Andy')).toEqual("Hi Andy");
  19. });
  20. it("do negative test with helloWorld function", function(){
  21. expect(helloworld.sayHi('Andy')).toContain("Andy");
  22. });
  23. });
  24. });

Step 4

Now change in SpecRunner.html , add JavaScript file (HelloWorld.js) and corresponding Spec file (spec/HelloWorldSpec.js).

SpecRunner.html


Now lets see the result.



Now I have changed my development cycle. As a result I'm saving time because I need not have to do redeployment.