JavaScript Testing

For All

Rushaine McBean

Client-Side Testing

Testing Workflow

The Importance of Testing

App Without Tests

App With Tests

Types of Testing

TDD

BDD

Client-Side Testing

Yes!, It can be hard.

What if I don’t know how to test the feature?


 	$(function() {
   	 // all logic in here
	  $('#searchForm').on('submit', function (e) {
	    e.preventDefault();
	    if (pending) { return; }
	    var form = $(this);
	    var query = $.trim( form.find('input[name="q"]').val() );
	    if (!query) { return; }
	    pending = true;
	    $.ajax('/data/search.json', {
	      data : { q: query },
	      dataType : 'json',
	      success : function (data) {
	        loadTemplate('people-detailed.tmpl').then(function (t) {
	          var tmpl = _.template(t);
	          resultsList.html( tmpl({ people : data.results }) );
	          pending = false;
	        });
	      }
	    });
   	);
 

Writing Testable Code

Use objects!

Object literals


var jqCon =  {
	sanDiegoDay1: function() {
		return "We had an awesome time!";
	}
    sanDiegoDay2: function() {
	   return "We heart JavaScript!";
    },
    entireConference: function() {
    	this.sanDiegoDay1();
    	this.sanDiegoDay2();
    }
};
jqCon.sanDiegoDay2();

Code Organization

Which tool to use?

Jasmine

What Is Jasmine?

Similar in Nature

How Do I Write a Jasmine Test?

Suites: "describe" Your Tests

    
    describe('jqCon.sanDiegoDay2', function() {
        it("state the action that will occur", function() {

        });
    });
    
    

Specs

They are the it() blocks inside of a suite. They describe what one small piece of the componment should do.


describe('jqCon.sanDiegoDay2', function() {
  describe("exucting the function", function() {
    it("should return some text", function() {
    });
  });
});
  

Expectations & Matchers

Expectations are built from the expect function

  
  describe("Hello world", function() {
    it("says world", function() {
      expect(helloWorld()).toContain("world");
    });
  });
  
  

Matchers are responsible for reporting to Jasmine if the expection is true or false

  
  describe("The 'toEqual' matcher", function() {
    it("works for simple literals and variables", function() {
      var a = 12;
      expect(a).toEqual(12);
    });
  });
  

Tooltips Spec

  
describe("SanDiego.toggleTooltip", function () {
  var clickEvent, doc, tooltip, tooltipTrigger, toolTipArea = null;

  describe("interacting with a tooltip", function() {

    describe("clicking on a closed tooltip", function() {
      it("should show the tooltip area", function() {
        tooltipTrigger = doc.find('#js-closed-tooltip');
        toolTipArea = doc.find('.js-closed-tooltip-area');
        SanDiego.toggleTooltip(tooltipTrigger, toolTipArea);
        expect($(toolTipArea)).not.toBeHidden();
      });
    });
  });

});

  

I SPY

Spies

 
    describe("clicking on a closed tooltip", function() {
      it("should trigger the click event", function() {
        clickEvent = spyOnEvent(tooltipTrigger, "click");
        $(tooltipTrigger).click();
        expect('click').toHaveBeenTriggeredOn(tooltipTrigger);
        expect(clickEvent).toHaveBeenTriggered();
      });
    });
  

DRY Test Suite

beforeEach() and afterEach()

 

  describe("clicking on a closed tooltip", function() {
    beforeEach(function () {
      tooltipTrigger = doc.find('#js-closed-tooltip');
      toolTipArea = doc.find('.js-closed-tooltip-area');

    });

    it("should trigger the click event", function() {
      clickEvent = spyOnEvent(tooltipTrigger, "click");
      $(tooltipTrigger).click();
      expect('click').toHaveBeenTriggeredOn(tooltipTrigger);
      expect(clickEvent).toHaveBeenTriggered();
    });

    it("should show the tooltip area", function() {
      jqCon.toggleTooltip(tooltipTrigger, toolTipArea);
      expect($(toolTipArea)).not.toBeHidden();
    });
  });
  

Nested Suites

  
  describe('jqCon.toggleTooltip', function() {
    describe("interacting with a tooltip", function() {

      describe("clicking on a closed tooltip", function() {

      });
      describe("clicking on a open tooltip", function() {

      });
    });
  });
  
  

Leveraging Additional Libraries

Introducing Jasmine-jQuery

Provides custom matchers for the jQuery library



toBe(jQuerySelector)
toBeChecked()
toBeEmpty()
toBeHidden()
toHaveCss()
toBeSelected()
toBeVisible()
toContain
toExist()
toHaveAttr(attributeName, attributeValue)
toHaveBeenTriggeredOn
toHaveBeenTriggered

Example of Jasmine jQuery matchers


describe("clicking on a closed tooltip", function() {
  beforeEach(function () {
    tooltipTrigger = doc.find('#js-closed-tooltip');
    toolTipArea = doc.find('.js-closed-tooltip-area');

  });

  it("should show the tooltip area", function() {
    LevoLeague.toggleTooltip(tooltipTrigger, toolTipArea);
    expect($(toolTipArea)).not.toBeHidden();
  });
});
    

HTML fixtures


    
some complex content here

    loadFixtures('myfixture.html');
    $('#my-fixture').myTestedPlugin();
    expect($('#my-fixture')).to...;
  

JSON Fixtures

Event Spies


      var tooltipTrigger = '#some_element';
       var clickEvent = spyOnEvent(tooltipTrigger, "click");
        $(tooltipTrigger).click();
        expect('click').toHaveBeenTriggeredOn(tooltipTrigger);
        expect(clickEvent).toHaveBeenTriggered();
      });

      

Introducing Jasmine-Ajax

Install the mock


    beforeEach(function() {
      loadFixtures('recommendations.html');
      jasmine.Ajax.useMock();
      doc = fixturesContainer();
    });

    

Trigger the ajax request


      describe("jqCon.sanDiegoDay2", function() {
        beforeEach(function() {
          loadFixtures('tooltip.html');
          jasmine.Ajax.useMock();
          doc = fixturesContainer();
        });

        describe("user is not logged in", function(){
          it("should not try to recommend", function(){
            jqCon.current_user = null;
            jqCon.sanDiegoDay2.init();
            link = doc.find("#recommendation.js-recommend");
            link.click();
            expect(mostRecentAjaxRequest()).toBeFalsy();
          });
        });
      });
    

Set responses


    describe("when the recommendation succeeds", function() {

      beforeEach(function() {
        var request = mostRecentAjaxRequest();
        request.response({status: 200, responseText: JSON.stringify({}) });
      });
    });
    

Inspect Ajax requests


          describe("jqCon.sanDiegoDay2", function() {
        beforeEach(function() {
          loadFixtures('tooltip.html');
          jasmine.Ajax.useMock();
          doc = fixturesContainer();
        });


    describe("when the undo succeeds", function() {

      beforeEach(function() {
        var request = mostRecentAjaxRequest();
        request.response({status: 200, responseText: JSON.stringify({}) });
      });

      it("removes the .recommended", function() {
        expect(link).not.toHaveClass('recommended');
      });

      it("decrements the count", function() {
        expect(link.find(".js-recommendation-count")).toHaveText('0');
      });

      it("re-enableds the link", function() {
        clearAjaxRequests();
        link.click();
        expect(mostRecentAjaxRequest()).not.toBeNull();
      });
    });
  });




      

Testing Workflow

(Loose) Laws of Testing

Local Development

Automation

Development Server

Maintainable JavaScript

Thanks!

Rushaine McBean