Unit Testing JavaScript Using Jasmine

What we'll cover

Intro to BDD (Behavior Driven Development)

In a BDD style test you describe your code and tell the test what it should be doing. Then you expect your code to do something.

Intro to BDD Continued...

//describe your code
describe ( 'presentation.js' , function(){
  
  //what it should do
  it ( 'should be informative', function(){
      
      //expect something 
      expect( 
          presentation.inform() 
      ).toBeTruthy();
  )};
} );

Note how describe + it reads like a sentence.

 

Red Green Refactor (TDD)

TDD & BDD go together like coffee shops and hipsters.

The key concept is to write your unit test before you write a line of implementation code.

Writing code this way helps you:

TDD The Purist Way

A true TDD purist will tell you to follow the following path to testing enlightenment:

  1. Write a failing test.
  2. Make the test pass as easily as possible.
  3. Try to break the test.

FizzBuzz

Lets take a look at a simple FizzBuzz example to so you what this looks like.

What is FizzBuzz?

Red

Spec

expect( kata.fizzBuzz( 3 ) )
    .toEqual( 'Fizz' );

fizzBuzz.js

var fizzBuzz : function( test ){
    //nothing is here yet
}

Green

The simplest implementation.

Spec

expect(kata.fizzBuzz(3)).toEqual('Fizz');

fizzBuzz.js

var fizzBuzz : function( test ){
    return "Fizz";
}

Refactor

Spec

expect(kata.fizzBuzz(3)).toEqual('Fizz');

expect(kata.fizzBuzz(0)).toEqual( '' );

fizzBuzz.js

var fizzBuzz : function( test ){
    return "Fizz";
}

Refactor

Spec

expect(kata.fizzBuzz(3)).toEqual('Fizz');

expect(kata.fizzBuzz(0)).toEqual( '' );

fizzBuzz.js

var fizzBuzz : function( test ){
  if ( test % 3 === 0 ){
      return 'Fizz';
  }
  return '';
}

Nobody actually has time for this, but its a noble goal to shoot for!

Writing Testable JS Code

This is NOT testable

$(document).ready(function(){
  var div1 = $('div.one'),
      itemCount = getItemCount( 'li.item' );

  div1.on('click',function(){
    ...
  });

  function getItemCount( selector ){
    return $( selector ).length;
  }
});

Why not?

Moar Testable

var app = {
  init : function(){
     div1.on(
        'click', this.handleDivClick
     );

     this.itemCount = 
        this.getItemCount( 'li.item' );
  },

  getItemCount : function( selector ){
    return $( selector ).length;
  },

  handleDivClick : function( e ){
    //this in here will still be div1
  }
};

Moar Testable

Now we can test our code to make sure:

Rebecca Murphey has an excellent presentation on "Writing Testable JavaScript" here: http://www.youtube.com/watch?v=OzjogCFO4Zo

Jasmine, so much more than a shrub.

Setup

Setup

  1. Download Jasmine here
  2. Unzip it into a folder in your project (i like to use a tests folder inside my js folder)
  3. Add your source files to SpecRunner.html
  4. Create spec files and add them to SpecRunner.html
  5. Open SpecRunner.html in a browser.

Folder Structure

js
|--tests
|  |--fixtures
|  |--spec
SpecRunner.html

SpecRunner.html

<!-- include source files here... -->
<script type="text/javascript" 
    src="src/Player.js"></script>
<script type="text/javascript" 
    src="src/Song.js"></script>

<!-- include spec files here... -->
<script type="text/javascript" 
    src="spec/SpecHelper.js"></script>
<script type="text/javascript" 
    src="spec/PlayerSpec.js"></script>

Failing Test Output

Failing Test Output

To show all tests click on the "5 Specs" text.

This is helpful when you have a lot of tests and you want to only run the "Describe" that has a failing test.

Passing Test Output

Specs

Specs

Specs

describe('fizzbuzz'){

  it(
    'should return fizz when the number is divisible by 3',
    function(){

      expect(
        kata.fizzBuzz(3)
      ).toEqual('Fizz'); //matcher

      expect(
        kata.fizzBuzz(0)
      ).toEqual( '' );
  });
  
});

Test Setup & Teardown

beforeEach( function (){
  //run before each "it"
});

afterEach( function (){
  //run after each "it"
});

Matchers

Matchers

toBe( 'expected' ) //exact compare (===)
toEqual( 'expected' ) //more general compare, can compare objects

Matchers

toBeUndefined( ) //checks for undefined
toBeDefined( ) //checks if var is not undefined

Matchers

toMatch( /regex/ ) //matches against regex
toBeNull( /regex/ ) //checks if a var is null

Matchers

toBeTruthy( ) //checks if var is truthy
toBeFalsy( ) //checks if var is falsy

Matchers

toBeLessThan( number ) //checks if value is less than number
toBeGreaterThan( number ) //checks if value is greater than number

Matchers

toContain( item ) //look for item in array
expect( function(){ fn(); }).toThrow( e ) //fn() should throw an error

not

Any matcher can be "reversed" by including the not keyword.

expect( 5 ).not.toEqual( 3 );

Jasmine jQuery

Jasmine jQuery

Jasmine jQuery is a set of matchers and functions that help you test DOM elements.

jQuery Matchers

Matchers

toBe( $('jQuerySelector') ) //compares results of one selector to another
toBeChecked( ) //radios and checkboxes

Matchers

toBeHidden( ) //display:none, hidden form elements, with & height = 0
toBeVisible( ) //checks for element to consume space on the document

Matchers

toBeSelected( ) //only for elements with selected attribute
toHaveCss( 'css' ) //pass in css like calling the jQuery css() fn

Matchers

toHaveAttr( 'attrName', 'attrValue' ) //checks for attribute on element
toConatin( $('jQuerySelector') ) //checks if selector is in children

Matchers

toHaveValue( value ) //checks if element has the value attribute equal to the value passed in
toHaveText( 'text' ) //checks if element has text

Matchers

toHaveId( 'id' ) //checks if element has id
toHaveHtml( 'html' ) //checks if element has html

Matchers

toHandle( 'eventname' ) //checks if element listens for event
toBeFocused( ) //checks if element has focus

Matchers

For a full list visit https://github.com/velesin/jasmine-jquery

Fixtures

Fixtures

Setting Up Fixtures

Jasmine jQuery assumes that they are in the path:

spec/javascripts/fixtures

Setting Up Fixtures

This path can be overridden by adding the following code to the SpecRunner.html file:

(function(){
    //this loads fixtures from a fixtures folder 
    //relative to where SpecRunner.html lives
    jasmine.getFixtures()
        .fixturesPath = 'fixtures/'; 
    
    var jasmineEnv = jasmine.getEnv();

    jasmineEnv.updateInterval = 1000;

Loading Fixtures

 beforeEach( function(){
    //fixture will be reset before each test
    loadFixtures( 'appFixture.html');
  } );

runs / waits

Runs / Waits

What if you have code like this?

hide : function(){
    $('.hidden').slideDown(5000);
}

runs / waits

Jasmine provides us with runs and waits functions for this:

runs( function(){ myObject.hide() }); //technically doesn't have to be in runs, but it better describe the intent

waits( 5100 );

runs( function(){
    expect( $('.hidden') ).not.toBeVisible();
});

runs / waits

When you have runs / waits functions Jasmine runs the first function, waits the duration of the waits function call, then calls the next runs function.

Good for jQuery animations, setTimeouts, setIntervals, or any other asynchronous and timed code.

Spies

Spies

Spies are utilities provided by jasmine to make sure a callback function has been called.

The following code is a good canidate for a spy:

hide : function(){
    var $ele = $('.hideMe'),
        self = this;

    $ele.fadeOut( function(){
        self.showOtherDiv
    });
}

Spies

To use it we tell Jasmine to spyOn something. By default they are similar to mocks in other unit testing frameworks.

it('must call the success callback', 
    function(){
        //overwrites showOtherDiv with the spy
        spyOn( myObj, 'showOtherDiv' ); 

        expect( myObj.showOtherDiv )
            .toHaveBeenCalled(); 
    }
);

Spy Matchers

expect( myObj.showOtherDiv ).toHaveBeenCalled(); 
//validates that the method was called
expect( myObj.showOtherDiv )
    .toHaveBeenCalledWith( args );
//validates that the method was called with specific arguments

Spy Metadata

myObj.showOtherDiv.calls.length //number of times called
expect( myObj.showOtherDiv.calls.length )
    .toEqual(2)

Spy Metadata

myObj.showOtherDiv.calls //array of calls
myObj.showOtherDiv.calls[0].args //array of args
myObj.showOtherDiv.mostRecentCall //get last call, has same metadata as other calls

Extending Spies

Spies can be extended to make your tests more powerful.

spyOn( myObj, 'populateData' )
    .andCallThrough(); //will create the spy but also call the original function
spyOn( myObj, 'populateData' )
    .andReturn( 'foobar' ); //simlar to a stub in other unit testing frameworks

Questions?

The End.

/

#