ASP.NET Connections – JavaScript Testing | John V. Petersen

:

I am honored to be presenting at ASP.NET Connections, March 26-29 in Las Vegas. One of the sessions I’m presenting is on the topic of JavaScript Testing. If you are writing web applications, JavaScript is likely as significant, if not more so, than your native C#/VB code. And yet, it  is often treated differently from a testing perspective. How does one organize JavaScript to make it testable? How do the SOLID principles apply? What tools can I use to test JavaScript? These questions and others will be addressed through practical code examples on how to use testing frameworks like QUnit and how those tools can be integrated into Visual Studio.

To more illustrate what this session will cover, consider the this hypothetical: we need to display address components in a pre-defined section of a page.

The end result might look like this:

The following code drives the address display:

$.getJSON("data/data.json", function (data) {}).success(function (data) {
		  $("#target").empty();
		  var items = [];
		  $.each(data, function (key, val) {
		    items.push('</pre>
<ul>
	<li id="' + key + '">' + val + '</li>
</ul>
<pre>
');
		  });

$('', {
		    html: items.join('')
		  }).appendTo('#target');

		})

		.error(function (error) {
		  $("#target").empty().append("

An error was encountered...

");
		})

		.complete(function (data) {
		  $("#target").removeClass('ajaxLoad');
		});

Fairly straightforward code: Data is acquired via getJSON and displayed. jQuery I used to drive the process. The code works, but there are problems. Recalling what we have learned from SOLID, DRY, etc. – we quickly learn that the code not maintainable, re-useable and it’s marginally testable. About the only thing that can be tested here is the end result. As to the target, were the proper classes assigned and is there an un ordered list with data? In other words, all we can really test are the side-effects of the JavaScript code, not the code itself. Looking at the code, there are several distinct things happening. First, there is the application of the ajaxLoad class. Second, there is the data acquisition process itself. Third, there is the application of the data. There are also operations to handle error conditions and when the ajax process has completed. Geographically, this code is embedded in an html document. Ideally, this code would be re-factored into its own js file. Going back to the distinct operations, each of these operations should be testable, and more specifically, unit testable. Ideally, we would like to verify our code with a test like this:

function testfixture() {
    module("When retrieving address data");
	//Arrange
	var html = $('</pre>
<div id="target"></div>
<pre>')
   //Act
   ApplySomeOperation(html);

    test("Given that the data has \
	  been applied to a pre-defined div",
	   function () {
	   expect(1);

	   var ul = $(html).find("ul");

       //Assert
	   equal(ul.length, 1, "The ul tag is present.");
	});
}

The code, as written, does not support a unit test like this. The code is essentially a big ball of mud. Again, the code “works”, but in our business, working is not enough. With a little re-factoring, we can get where we need to be.

This is what we end up with:

function removeStartMsg(target) {
   $(target).removeClass('ajaxLoad');
}
function displayStartMsg(target) {
   $(target)
      .empty()
      .addClass('ajaxLoad')
      .append("

One moment while your data is \
	    being retrieved...

");
}
function createAddressHtml(data) {
   var items = [];
      $.each(data, function(key, val) {
	     items
		   .push('</pre>
<ul>
	<li id="' + key + '">'
 + val + '</li>
</ul>
<pre>
');
	  });

return $('', {
		html: items.join('')
	   })
}
function displayAddress(data,target) {
   target.empty().html(createAddressHtml(data));
}
function getAddress(id,target) {
	var complete =  function(data) {
	    removeStartMsg(target);
		$(target).removeClass('ajaxLoad');
	};

	var success = function(data) {
		displayAddress(data,target)
	};

	var error = function(error) {
	   $(target).empty().append("
An error was encountered...

");
	}
	displayStartMsg(target);
	getData(id,"data/data.json",success,error,complete);
}
function getData(id,source,successCallBack,errorCallBack,completeCallBack) {
   $.getJSON(source, {id: id}, 
       function (data) {}).success(function (data) {
	   successCallBack(data)
   })
   .error(function (error) {
       errorCallBack(error)
   })
   .complete(function (data) {
   	   completeCallBack(data)
	});
}

The idea is that each discrete operation has its own function. Thus – we adhere to the single responsibility principle. Also note that no function reaches into the DOM. Instead, the object it is to act on is handed to it. The code in the page reduces to this:

<script type="text/javascript">
   getAddress(0,$("#target"));
</script>

For testing purposes, the json source is a static file on IIS. With little effort however, the source could easily be any http endpoint. With the refactored code, we can write more granular tests.

For example:

function testfixture() {
    module("When retrieving address data");
	//Arrange
	var html = $('<div id="target"></div>')
   //Act
   
   getAddress(0,html);
   stop(2); // allow async processes to run

    test("Given that the data has \
	  been applied to a pre-defined div", 
	   function () {
	   expect(1);
	   
	   var ul = html.find("ul");
	   
       //Assert   
	   equal(ul.length, 1, "The ul tag is present.");
	});
	
    test("Given that the data has \
	  been applied to a pre-defined div", 
	   function () {
	   expect(1);
	   var expectedHtml = "<ul><li id=\"company\">Microsoft Corporation</li>
                <li id=\"Address\">One Microsoft Way</li><li id=\"City\">Redmond</li>
                <li id=\"State\">WA</li><li id=\"Zip\">98052</li></ul>"
       //Assert   
	   equal(html.html(), expectedHtml, "The html markup is correct.");
	});
}

The following is the QUnit output:

Again, with the refactored code, we can test everything, at a unitary level, that is needed to support the display.