Book Image

JavaScript Unit Testing

By : Hazem Saleh
Book Image

JavaScript Unit Testing

By: Hazem Saleh

Overview of this book

<p>The largest challenge for many developers’ day to day life is ensuring the support of, and assuring the reach of, their product. With the ever increasing number of mainstream web browsers this is becoming a more difficult task for JavaScript coders. <br /><br />From the beginning, JavaScript Unit Testing will show you how to reduce the time you spend testing, and automate and ensure efficiency in guaranteeing your success.<br /><br />JavaScript Unit Testing will introduce and help you master the art of efficiently performing and automating JavaScript Unit tests for your web applications.<br /><br />Using the most popular JavaScript unit testing frameworks, you will develop, integrate, and automate all the tests you need to ensure the widest reach and success of your web application.<br /><br />Covering the most popular JavaScript Unit testing frameworks of today, JavaScript Unit Testing is your bible to ensuring the functionality and success of all of your JavaScript and Ajax Web Applications.<br /><br />Starting with Jasmine, you will also learn about, and use, YUITest, QUnit, and JsTestDriver, integrate them into your projects, and use them together to generate reports.<br /><br />Learn to automate these tools, make them work for you, and include the power of these tools in your projects from day one.</p>
Table of Contents (12 chapters)

Weather forecasting application


Now, let's move to the weather forecasting application. The weather forecasting application is a Java web application that allows the users to check the current weather in different cities in the world. The weather forecasting application contains both synchronous and asynchronous (Ajax) JavaScript code, which we will test in the later chapters of the book using the different JavaScript unit testing frameworks.

The weather forecasting application mainly contains three use cases:

  • Log in to the application

  • Register a user in the application

  • Check the current weather in a specific city

The weather forecasting application is a Java web application. The server-side part of the application is written using Java servlets (http://docs.oracle.com/javaee/6/tutorial/doc/bnafd.html). If you are not familiar with Java servlets, do not worry. This book focuses only on JavaScript unit testing; all you need to know about these servlets is the functionality of each one of them, not the code behind it. The functionality of each application servlet will be explained during when the JavaScript code is explained, to show you the complete Ajax request life cycle with the server.

Another thing that needs to be mentioned is that the weather application pages are .jsp files; however, 99 percent of their code is pure HTML code that is easy to understand (the application pages code will be explained in detail in the next section).

The first screen of the application is the login screen in which the user enters his username and password, as shown in the following screenshot:

When the user clicks on the Login button, there is a JavaScript login client that ensures that the username and the password are entered correctly. If the username and the password are correct, they are submitted to the server, which validates them if the user is registered in the application. If the user is registered in the application then the user is redirected to the weather checking page; otherwise an error message appears to the user.

The username field must not be empty and has to be in a valid e-mail address format. The password field also must not be empty and has to contain at least one digit, one capital, one small character, and at least one special character. The password length has to be six characters or more.

In the weather checking page, the user can select one of the available cities from the combobox, then click on the Get weather condition button to get the current weather information of the selected city, as shown in the following screenshot:

In the user registration page, the user can register in the application by entering his username and confirmed password, as shown in the following screenshot:

When the user clicks on the Register button, the registration client's JavaScript object ensures that the username and the passwords are entered correctly. The registration client uses the same rules of the login client in username and password validations. It also ensures that the confirmed password is the same as the entered password.

If the user's registration information is correct, the username and passwords are submitted to the server. The user information is registered in the system after performing server-side validations and checking that the user has not already registered in the application. If the user is already registered in the system then an error message appears to the user.

Exploring the application's HTML and JavaScript code

The following code snippet shows the HTML code of the login form in the login.jsp file. It is a simple form that has username and password fields with their labels, messages, a registration link, and a login button.

<form class="box login" action="/weatherApplication/LoginServlet" method="post">
  <fieldset class="boxBody">    
    <label for="username">Username  <span id="usernameMessage" class="error"></span></label>
    <input type="text" id="username" name="username"/>
    
    <label for="password">Password  <span id="passwordMessage" class="error"></span></label>
    <input type="password" id="password" name="password"/>
  </fieldset>        
  <div id="footer">
    <label><a href="register.jsp">Register</a></label>    
    <input id="btnLogin" class="btnLogin" type="submit" value="Login" onclick="return validateLoginForm();"/>      
  </div>        
</form>

When the Login button is clicked, the validateLoginForm JavaScript function is called. The following code snippet shows the validateLoginForm function in the login.jsp file:

function validateLoginForm() {
  var loginClient = new weatherapp.LoginClient();
  
  var loginForm = {
    "userNameField" : "username",
    "passwordField" : "password",
    "userNameMessage" : "usernameMessage",
    "passwordMessage" : "passwordMessage"
  };
  
  return loginClient.validateLoginForm(loginForm);
}

The validateLoginForm function calls the LoginClient JavaScript object that is responsible for validating the login form. It constructs a JavaScript Object Notation (JSON) object that includes the username, password, username message, and password message IDs, and then passes the constructed JSON object to the validateLoginForm function of the LoginClient object.

Note

The weather application customizes a CSS3 based style from the blog CSS Junction:

http://www.cssjunction.com/freebies/simple-login-from-html5css3-template-free/

The following code snippet shows the validateLoginForm method of the LoginClient object in the LoginClient.js file. It validates that the username and the password fields are not empty and are compliant with the validation rules.

if (typeof weatherapp == "undefined" || !weatherapp) {
  weatherapp = {};
}

weatherapp.LoginClient = function() {};

weatherapp.LoginClient.prototype.validateLoginForm =  function(loginForm) {
  
  if (this.validateEmptyFields(loginForm) && 
      this.validateUserName(loginForm) && 
        this.validatePassword(loginForm)) {
    
    return true; 
  } 
  
  return false;
};

Tip

One of the recommended JavaScript's best practices is to use namespaces; the application defines a JavaScript namespace in order to avoid collisions with other JavaScript objects of similar names. The following code defines a weatherapp namespace if it is not already defined:

if (typeof weatherapp == "undefined" || !weatherapp) {
  weatherapp = {};
}

The following code snippet shows the validateEmptyFields method of the LoginClient object in the LoginClient.js file. It validates that the username and the password fields are not empty and if any of these fields are empty, an error message appears:

weatherapp.LoginClient.prototype.validateEmptyFields =  function(loginForm) {
  var passwordMessageID = loginForm.passwordMessage;
  var userNameMessageID = loginForm.userNameMessage;
  
  var passwordFieldID = loginForm.passwordField;
  var userNameFieldID = loginForm.userNameField;
  
  document.getElementById(passwordMessageID).innerHTML = "";
  document.getElementById(userNameMessageID).innerHTML = "";  
  
  if (! document.getElementById(userNameFieldID).value) {
    document.getElementById(userNameMessageID).innerHTML = "(field is required)";
    
    return false;
  }
  
  if (! document.getElementById(passwordFieldID).value) {
    document.getElementById(passwordMessageID).innerHTML = "(field is required)";

  return false;
  }
  
  return true;
};

The following code snippet shows the validateUserName method of the LoginClient object in the LoginClient.js file. It validates that the username is in the form of a valid e-mail:

weatherapp.LoginClient.prototype.validateUserName = function(loginForm) {

  // the username must be an email...
  var userNameMessageID = loginForm.userNameMessage;
  var userNameFieldID = loginForm.userNameField;  

var userNameRegex = /^[_A-Za-z0-9-]+(\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,})$/;  
  var userName = document.getElementById(userNameFieldID).value;

if(! userNameRegex.test(userName)) {
    document.getElementById(userNameMessageID).innerHTML = "(format is invalid)";
 
return false;
}  
    
    return true;    
};

Using the regular expression /^[_A-Za-z0-9-]+(\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,})$/, the username is validated against a valid e-mail form. If the username is not in a valid e-mail form then an error message appears in the username message span.

The following code snippet shows the validatePassword method of the LoginClient object in the LoginClient.js file. It validates if the password has at least one digit, one capital character, one small character, at least one special character, and also if it contains six characters or more:

weatherapp.LoginClient.prototype.validatePassword = function(loginForm) {
  
  // the password contains at least one digit, one capital and small character 
  // and at least one special character, and 6 characters or more...
  var passwordMessageID = loginForm.passwordMessage;
  var passwordFieldID = loginForm.passwordField;
  
  var passwordRegex = /((?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{6,20})/;
  var password = document.getElementById(passwordFieldID).value;  
  
  if (! (passwordRegex.test(password) && password.length >= 6)) {
    document.getElementById(passwordMessageID).innerHTML = "(format is invalid)";
    
    return false;
  }  
  
  return true;
};

If the password is not compliant with the mentioned rules then an error message appears in the password message span.

If the username and the password fields pass the JavaScript validation rules, the login form submits its content to LoginServlet, which makes another server-side validation and then redirects the user to the weather checking page if the validation goes OK.

Tip

It is very important not to rely on the JavaScript client-side validation only, because JavaScript can be disabled from the browser. So it is a must to always make a server-side validation besides the client-side validation.

The following code snippet shows the weather checking form of the weather application located in the welcome.jsp file. It contains a combobox filled with the Yahoo! Weather Where On Earth IDs (the WOEID is a unique reference identifier assigned by Yahoo! to identify any place on Earth) of different cities in the world.

<h1>Welcome to the weather application</h1>
<FORM method="post">
  <label class="label" for="postalCode">Select the Location: </label>
  <select id="w" class="selectField">
    <option value="1521894">Cairo, Egypt</option>
    <option value="906057">Stockholm, Sweden</option>  
    <option value="551801">Vienna, Austria</option>
    <option value="766273">Madrid, Spain</option>  
    <option value="615702">Paris, France</option>
    <option value="2459115">New York, USA</option>
    <option value="418440">Lima, Peru</option>            
  </select>     
  
  <input type="button" class="button" onclick="invokeWeatherClient();" 
             value="Get weather condition"/>
  <br/><br/>
             
  <div id="weatherInformation" class="weatherPanel">
  </div>
</FORM>

When the Get weather condition button is clicked, the invokeWeatherClient function is called. The following code snippet shows the invokeWeatherClient function code in the welcome.jsp file:

function invokeWeatherClient() {
  var weatherClient = new weatherapp.WeatherClient();
  var location = document.getElementById("w").value;  
  
  weatherClient.getWeatherCondition({
    'location': location,
    'resultDivID': 'weatherInformation'
  }, 
weatherClient.displayWeatherInformation, 
weatherClient.handleWeatherInfoError);    
}

The invokeWeatherClient function calls the getWeatherCondition method of the WeatherClient object. The first parameter of the getWeatherCondition method is the weatherForm object, which is a JSON object containing the location WOEID and the ID of the DIV element that receives the weather information HTML result of the Yahoo! Weather Representational State Transfer (REST) service. The second parameter represents the first callback, which is the displayWeatherInformation method that is called if the getWeatherCondition call succeeds. The last parameter represents the second callback, which is the handleWeatherInfoError method that is called if the getWeatherCondition call fails.

The following code snippet shows getWeatherCondition of the WeatherClient object in the WeatherClient.js file that sends an Ajax request to WeatherProxyServlet with the w parameter that represents the WOEID. WeatherProxyServlet interacts with the Yahoo! Weather REST service in order to fetch the current weather information:

if (typeof weatherapp == "undefined" || !weatherapp) {
  weatherapp = {};
}

weatherapp.WeatherClient = function() {};
weatherapp.WeatherClient.xmlhttp;
weatherapp.WeatherClient.weatherForm;
weatherapp.WeatherClient.endpointURL = "";

weatherapp.WeatherClient.prototype.getWeatherCondition =  function(weatherForm, successCallBack, failureCallBack) {
  
  if (window.XMLHttpRequest) {
    this.xmlhttp = new XMLHttpRequest();
  } else {
    this.xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
  }
  
  var successCallBackLocal = successCallBack;
  var failureCallBackLocal = failureCallBack;  
  var weatherClientLocal = this;
  
  this.xmlhttp.onreadystatechange = function() {
weatherClientLocal.weatherInformationReady(successCallBackLocal, failureCallBackLocal);
  };
  
  this.weatherForm = weatherForm;
  
  if (typeof this.endpointURL == "undefined") {
    this.endpointURL = "";
  }
  
  this.xmlhttp.open("GET", 
          this.endpointURL + 
          "/weatherApplication/WeatherProxyServlet?w=" + weatherForm.location + "&preventCache=" + new Date().getTime(), 
          true);
  
  this.xmlhttp.send();  
};

weatherapp.WeatherClient.prototype.weatherInformationReady =  function(successCallBack, failureCallBack) {  
  if (this.xmlhttp.readyState != 4) { 
    return; 
  }
  
  if (this.xmlhttp.status != 200)  {
    failureCallBack(this);

return;
      }
    
  if (this.xmlhttp.readyState == 4 && this.xmlhttp.status == 200) {
    successCallBack(this);
      }
};

weatherapp.WeatherClient.prototype.displayWeatherInformation =  function(weatherClient) {
  var resultDivID = weatherClient.weatherForm.resultDivID;
  
document.getElementById(resultDivID).innerHTML = weatherClient.xmlhttp.responseText;
};

weatherapp.WeatherClient.prototype.handleWeatherInfoError =  function(weatherClient) {
  var resultDivID = weatherClient.weatherForm.resultDivID;
  
  alert ("Error: " + weatherClient.xmlhttp.responseText);  
document.getElementById(resultDivID).innerHTML = "Error: " + weatherClient.xmlhttp.responseText;  
};

The getWeatherCondition method first creates an XML HTTP request object using new XMLHttpRequest() in case of IE7+, Firefox, Chrome, and Opera. In the case of IE5 and IE6, the XML HTTP request object is created using an ActiveX object new ActiveXObject("Microsoft.XMLHTTP").

The getWeatherCondition method then registers both, the success callback (successCallBack) and the failure callback (failureCallBack) using the weatherInformationReady method that is called for every Ajax readyState change.

Finally, the getWeatherCondition method sends an asynchronous Ajax request to WeatherProxyServlet. When the Ajax response comes from the server and the operation is done successfully then the success callback is called, which is the displayWeatherInformation method. In the case of operation failure (which can happen, for example, if the passed WOEID is invalid or the Yahoo! Weather service is down), the failure callback is called, which is the handleWeatherInfoError method.

The displayWeatherInformation method displays the returned weather information HTML result from WeatherProxyServlet (which fetches the weather information from the Yahoo! Weather REST service) in the weatherInformation div element while the handleWeatherInfoError method displays the error message in the same div element and also displays an alert with the error message.

Tip

It is assumed that you are familiar with Ajax programming. If you are not familiar with Ajax programming, it is recommended to check the following introductory Ajax tutorial on w3schools:

http://www.w3schools.com/ajax/default.asp

In order to prevent IE from caching Ajax GET requests, a random parameter is appended using new Date().getTime(). In many JavaScript libraries, this can be handled through the framework APIs. For example, in Dojo the preventCache attribute of the dojo.xhrGet API can be used to prevent the IE Ajax GET caching.

The following code snippet shows the HTML code of the registration form in the register.jsp file. It consists of a username and two password fields with their corresponding labels, messages, login link, and a register button:

<form class="box register" method="post">      
  <fieldset class="boxBody">      
        
<label for="username">Username (Email)  <span id="usernameMessage" class="error"></span></label>
    <input type="text" id="username" name="username"/>
    
<label for="password1">Password  <span id="passwordMessage1" class="error"></span></label>
    <input type="password" id="password1" name="password1"/>

    <label for="password2">Confirm your password</label>
    <input type="password" id="password2" name="password2"/>
    
  </fieldset>        
  <div id="footer">
    <label><a href="login.jsp">Login</a></label>    
<input id="btnRegister" class="btnLogin" type="button" value="Register" onclick="registerUser();" />
  </div>      
  
</form>

When the Register button is clicked, the registerUser JavaScript function is called. The following code snippet shows the code of the registerUser function in the register.jsp file:

function registerUser() {
  var registrationClient = new weatherapp.RegistrationClient();
  
  var registrationForm = {
    "userNameField" : "username",
    "passwordField1" : "password1",
    "passwordField2" : "password2",        
    "userNameMessage" : "usernameMessage",
    "passwordMessage1" : "passwordMessage1"
  };
  
  if (registrationClient.validateRegistrationForm(registrationForm)) {
    
    registrationClient.registerUser(registrationForm, 
                     registrationClient.displaySuccessMessage, 
                          registrationClient.handleRegistrationError);
  }
}

The registerUser function is calling the RegistrationClient JavaScript object that is responsible for validating and submitting the registration form using Ajax to RegistrationServlet. registerUser constructs the registrationForm JSON object, which includes the username, password1, password2, username message, and password1 message IDs, and then passes the object to the validateRegistrationForm method of the RegistrationClient object.

If the validation passes, it calls the registerUser method of the RegistrationClient object. The first parameter of the registerUser method is the registrationForm JSON object. The second parameter is the success callback, which is the displaySuccessMessage method, while the last parameter is the failure callback, which is the handleRegistrationError method.

The following code snippet shows the code of the validateRegistrationForm method of the RegistrationClient object in the RegistrationClient.js file. It uses the validation methods of LoginClient in order to validate the empty username and password fields, and to validate if the username and the password fields conform to the validation rules. In addition to this, the validateRegistrationForm method validates if the two entered passwords are identical:

if (typeof weatherapp == "undefined" || !weatherapp) {
  weatherapp = {};
}

weatherapp.RegistrationClient = function() {};
weatherapp.RegistrationClient.xmlhttp;
weatherapp.RegistrationClient.endpointURL = "";

weatherapp.RegistrationClient.prototype.validateRegistrationForm =  function(registrationForm) {  
  var userNameMessage = registrationForm.userNameMessage;  
  var passwordMessage1 = registrationForm.passwordMessage1;
  
  var userNameField = registrationForm.userNameField;  
  var passwordField1 = registrationForm.passwordField1;
  var passwordField2 = registrationForm.passwordField2;  
  
  var password1 = document.getElementById(passwordField1).value;
  var password2 = document.getElementById(passwordField2).value;
  
  // Empty messages ...
  document.getElementById(userNameMessage).innerHTML = "";  
  document.getElementById(passwordMessage1).innerHTML = "";  
  
  // create the loginClient object in order to validate fields ...
  var loginClient = new weatherapp.LoginClient();
  
  var loginForm = {};
  
  loginForm.userNameField = userNameField;
  loginForm.userNameMessage = userNameMessage;
  loginForm.passwordField = passwordField1;
  loginForm.passwordMessage = passwordMessage1;    
  
  // validate empty username and password fields.
  if (! loginClient.validateEmptyFields(loginForm)) {
    return false;
  }
  
  // validate that password fields have the same value...
  if (password1 != password2) {
document.getElementById(passwordMessage1).innerHTML = "(Passwords must be identical)";
  
    return false;
  }
  
  // check if the username is correct...  
  if (! loginClient.validateUserName(loginForm) ) {
document.getElementById(userNameMessage).innerHTML = "(format is invalid)";
    
    return false;
  }
  
  // check if the password is correct...
  if (! loginClient.validatePassword(loginForm) ) {
document.getElementById(passwordMessage1).innerHTML = "(format is invalid)";
    
    return false;
  }  
  
  return true;    
};

The following code snippet shows the registerUser method code of the RegistrationClient object in the RegistrationClient.js file. It creates an Ajax POST request with the username and the passwords' (original password and confirmed password) data and sends them asynchronously to RegistrationServlet.

weatherapp.RegistrationClient.prototype.registerUser =  function(registrationForm, successCallBack, failureCallBack) {
  var userNameField = registrationForm.userNameField;  
  var passwordField1 = registrationForm.passwordField1;
  var passwordField2 = registrationForm.passwordField2;  
  
  var userName = document.getElementById(userNameField).value;    
  var password1 = document.getElementById(passwordField1).value;
  var password2 = document.getElementById(passwordField2).value;
  
  if (window.XMLHttpRequest) {
    this.xmlhttp = new XMLHttpRequest();
  } else {
    this.xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
  }
  
  var successCallBackLocal = successCallBack;
  var failureCallBackLocal = failureCallBack;  
  var registrationClientLocal = this;
  
  this.xmlhttp.onreadystatechange = function() {
registrationClientLocal.registrationReady(successCallBackLocal, failureCallBackLocal);
  };
  
  if (typeof this.endpointURL == "undefined") {
    this.endpointURL = "";
  }
  
  this.xmlhttp.open("POST", 
            this.endpointURL + 
            "/weatherApplication/RegistrationServlet", 
             true);
  
this.xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
  
  this.xmlhttp.send(userNameField + "=" + userName + "&" +
            passwordField1 + "=" + password1 + "&" +
            passwordField2 + "=" + password2);  
};

weatherapp.RegistrationClient.prototype.registrationReady =  function(successCallBack, failureCallBack) {  
  if (this.xmlhttp.readyState != 4) { 
    return; 
  }
  
  if (this.xmlhttp.status != 200)  {
    failureCallBack(this);
return;
}
    
  if (this.xmlhttp.readyState == 4 && this.xmlhttp.status == 200) {
    successCallBack(this);
      }
};

weatherapp.RegistrationClient.prototype.displaySuccessMessage =  function(registrationClient) {
  alert("User registration went successfully ...");
};

weatherapp.RegistrationClient.prototype.handleRegistrationError =  function(registrationClient) {
  alert(registrationClient.xmlhttp.responseText);
};

RegistrationServlet validates the user data and ensures that the user did not already register in the application. When the Ajax response comes from the server, and the registration operation is completed successfully, the displaySuccessMessage method is called. If the registration operation failed (for example, if the user ID is already registered in the application), the handleRegistrationError method is called. Both the displaySuccessMessage and the handleRegistrationError methods display alerts to show the success and the failure registration messages.

Running the weather application

In order to run the weather application, you first need to download the weatherApplication.war file from the book's website (www.packtpub.com). Then you need to deploy the WAR file on Apache Tomcat 6. In order to install Apache Tomcat 6, you need to download it from http://tomcat.apache.org/download-60.cgi. Apache Tomcat 6.0 requires the Java 2 Standard Edition Runtime Environment (JRE) Version 5.0 or later.

In order to install JRE, you need to download and install the J2SE Runtime Environment as follows:

  1. Download the JRE, release Version 5.0 or later, from http://www.oracle.com/technetwork/java/javase/downloads/index.html.

  2. Install the JRE according to the instructions included with the release.

  3. Set an environment variable named JRE_HOME to the pathname of the directory in which you installed the JRE, for example, c:\jre5.0 or /usr/local/java/jre5.0.

After you download the binary distribution of Apache Tomcat 6, you need to unpack the distribution in a suitable location on the hard disk. After this, you need to define the CATALINA_HOME environment variable, which refers to the location of the Tomcat distribution.

Now, you can start Apache Tomcat 6 by executing the following command on Windows:

$CATALINA_HOME\bin\startup.bat 

Start as while in Unix, you can execute the following command:

$CATALINA_HOME/bin/startup.sh

In order to make sure that the Apache Tomcat 6 starts correctly, you need to type the following URL in the browser:

http://localhost:8080/

After making sure that the Apache Tomcat 6 is running correctly, you can stop it by executing the following command on Windows:

$CATALINA_HOME\bin\shutdown.bat  

Start as while in Unix, you can execute the following command:

$CATALINA_HOME/bin/shutdown.sh

Now, we come to the step of the weather application deployment where you need to get the weatherApplication.war file from the book resources. After getting the file, copy the WAR file to the $CATALINA_HOME\webapps folder, then start the Apache Tomcat 6 again.

In order to access the weather application, you can access it using the following URL:

http://localhost:8080/weatherApplication/login.jsp

Tip

For the sake of simplicity, there is a predefined username and password that can be used to access the weather application; the username is [email protected] and the password is Admin@123. Another thing that has to be mentioned is that the registered users are not stored in a database; they are stored in the application scope, which means they will be available as long as the application is not restarted.