Friday, March 29, 2024

Using Jasmine 2.0’s New done() Function to Test Asynchronous Processes

Using Jasmine 2.0’s New done() Function to Test Asynchronous Processes

Just recently, I wrote about Jasmine 2.0 in my Testing DOM Events Using jQuery and Jasmine 2.0 article. It described how to use spies to verify that a given event was triggered and that it resulted in the expected action. While adequate for simple events such as a message being displayed and the like, spies by themselves aren’t enough to test longer asynchronous processes. For those, you need to use the new done() method. It replaces the runs() and waitsFor() methods of earlier jasmine versions to make it more similar to other testing frameworks. In today’s article, we’re going to learn how to use Jasmine 2.0‘s done() method to test DOM events without polluting the test subject page with test code.

Using the done() Function

The done() function is always passed to the beforeEach(), afterEach(), and it() test methods as an argument, whether you need it or not. To use it, include the done argument to the method and the call it after all of the processing is complete. This will usually be in the success callback function of Ajax calls and the pertinent event listener of DOM events. To illustrate, here is a unit test for menu retrieval. The first beforeEach() does not include the done function because there is no asynchronous processing taking place within it. However, the beforeEach() of the nested describe (“when retrieved by name”) does because the menus’ getMenuByName() function is asynchronous. The done() call is made within the success() callback function to instruct jasmine that beforeEach() has terminated and it is now safe to continue with the it() function. There are no asynchronous events in the it() function, so the done() function is not utilized, although we could include it if we needed to.

describe("Menu Tests", function() {
  var menus;
  
  beforeEach(function() { 
    menus = new Menus();
    menus.add(new Menu({
        name:   'dinner',
        season: 'Fall'
    }));
  });
  
  describe("when retrieved by name", function() { 
    var menu;
    
    beforeEach(function(done) {
      menu = menus.getMenuByName('dinner', {
        success: function () {
          done();
        }
      });
    });
  
    it("should have a valid season", function() { 
      expect(menu.season).toEqual("Fall");
    });
  });
});

The Test Page

The following jQuery-enabled web page contains two DIVs: one to display messages and one that shows restaurant menus. On page load, it fetches new menus from the server and includes a link to each using the restaurant name as the item name. Our tests will verify that the appending of the new menus works as expected.

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>Target Page</title>
		<style>
			#results {
				font-size: 1.2 rem;
				margin: 10px;
				height: 50px;
			}

			#content-area {
				background-color: #FFFFFF;
				height: 100px;
				padding-left: 5px;
				width: 200px;
			}
		</style>
		<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
		<script type="text/javascript" src="scripts/menus.js"></script>
	</head>
	<body>
		<div id="results"></div>
		<div id="content-area"></div>
	</body>
</html>

In the menus.js file, the getNewMenus() function is invoked on the page’s onload event. Inside the event handler, an Ajax get() call retrieves the restaurant data from the server. Presumably, a server-side process would handle the data retrieval, but for the sake of simplicity, this call simply reads a text file and appends the HTML contents into the content-area DIV via the successCallback() method. Our unit test will cover the end-to-end process from the page load to the new restaurant being inserted into the DOM. Since there is no way of knowing how long that will take, it is an ideal candidate for the done() function.

function successCallback(data) { 
  $( "#content-area" ).append( data );  //if this doesn't work try data.children[0]
}

function failCallback(data) { 
  $( "#results" ).text( "Load new menus failed." );  
}

function getNewMenus() {
  //formatted like <p><a href="/restaurant/Robs_Bistro/#title">Robs Bistro</a></p>
  $.get( "new menus.txt", successCallback).fail(failCallback);
}

$(document).ready(function($) {
  getNewMenus();
});

The SpecRunner HTML page

Jasmine unit tests run in a page called the Spec Runner. It references the required jasmine, jQuery, and jasmine-jquery libraries, as well as the menus.js script file, which we’ll be testing.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>DOM Tests using Jasmine v2.0</title>
  <link rel="shortcut icon" type="image/png" href="images/jasmine_favicon.png">
  <link rel="stylesheet" type="text/css" href="scripts/jasmine-master/lib/jasmine-core/jasmine.css">

  <script type="text/javascript" src="scripts/jasmine-master/lib/jasmine-core/jasmine.js"></script>
  <script type="text/javascript" src="scripts/jasmine-master/lib/jasmine-core/jasmine-html.js"></script>
  <script type="text/javascript" src="scripts/jasmine-master/lib/jasmine-core/boot.js"></script>
  <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
  <script type="text/javascript" src="scripts/jasmine-master/lib/jasmine-core/jasmine-jquery.js"></script>
  
  <script type="text/javascript" src="scripts/menus.js"></script>
  <script type="text/javascript">
    //test code goes here
  </script>
</head>
<body>
</body>

The Test Code

We’ll need access to the DOM because we have to check that the new menu link was appended to the contents-area DIV. An easy way to access the DOM without altering the original page is to use the jasmine-jquery library’s setFixtures() method. It accepts an HTML string but treats it as a proper DOM object allowing us to run tests against it. In fact, you can append the whole test page in there without performing any extra parsing. Getting the HTML code to insert can be accomplished using Ajax. In the success callback, we can pass the data directly to setFixtures().

Due to the numerous Ajax calls, the it() function won’t know when to begin, which will result in a timeout. Therefore, we need to insert a call to done() at the end of the getNewMenus() Ajax success callback function because it’s the most likely place in the chain of events that the DOM has been updated. JavaScript is versatile enough that you could alter the original function on the fly and include the done() call on the last line. However, there’s an easier way. If you’re familiar with Aspect Oriented Programming (AOP), you are no doubt aware that it is possible to wrap a proxy object around the original function so that it includes the extra code. The wrapper basically contains a call to the original function, followed by the done() invocation.

  //test code
  describe("Test GetNewMenus()", function() {
  	beforeEach(function(done) {	 
  		$.ajax({
		   url: 'targetPage3.html',
		   data: {},
		   success: function (data) {
				  setFixtures(data);
		  		successCallback = (function(originalFunction) {
    	      return function(data) {
        			originalFunction(data);
        			done();
        		};
      	 })(successCallback);
		   },
		   dataType: 'html'
		  });
   });
 });

The it() function contains several expectation matchers form the excellent jasmine-jquery library. We used it in the last article as well. Using it, we can verify that the listEntry exists, is in the DOM, and contains an <a> element that includes the restaurant name and the expected URL.

   
it ("should fetch the new restaurant meta data from the server.", function() {
    var listEntry = $("#content-area > p").last();
    expect(listEntry).toExist();
    expect(listEntry).toContainElement('a');
    expect(listEntry).toContainText("Robs Bistro");
    expect(listEntry.children('a:first')).toHaveAttr('href', '/restaurant/Robs_Bistro/#title');
});

Here is a screenshot that shows the fixtures as they appear in Firefox.

The fixtures are replaced by the results after a successful test run.

Conclusion

Jasmine is one of the most successful JavaScript testing libraries out there, and for good reason. It’s equally well suited for GUI testing as well as background business processing. There were some rumblings about the use of the runs() and waitsFor() functions, but with the release of 2.0, those complaints should be put to rest.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured