Click here to Skip to main content
15,894,825 members
Articles / Web Development / ASP.NET

Integrating Jasmine and Selenuim for Web UI Testing (ASP.NET)

Rate me:
Please Sign up or sign in to vote.
4.83/5 (10 votes)
15 Mar 2014CPOL16 min read 42.1K   461   24   8
This article provides a short introduction of integrating Jasmine and Selenium to test web pages.

Introduction

Testing a software application is as important as its development. We all know about the benefits of testing, and so many testing frameworks are developed in the recent years to support this critical aspect of software development. Web applications are very popular, and using JavaScript is almost unavoidable in any web project. There are many JavaScript testing frameworks for testing JavaScript based applications, but it is still hard to test ASP.NET web user interfaces. In this article, we are going to show how to integrate Jasmine framework and Selenium in ASP.NET projects. This helps to automate the user interface testing and speed up the testing process in different browsers.

Audiences

This article is for web application developers, who are familiar with JavaScript and testing, and would like to integrate automated testing of their web user interfaces to their development tasks.

PHP and JSP (and any other web) developers can also use the JavaScript testing part of this article in their project.

Background

Microsoft is like a family you grow up in it. They do their best to fulfill your every need, but as you grow up, your needs grow up with you, and you have to fulfill them in a larger society (i.e. Open-source community). It is exactly the time that you start to realize the usefulness and importance of these communities. :)

Microsoft still doesn't offer any comprehensive solution for ASP.NET UI testing and JavaScript testing. Microsoft Coded UI is very limited and third party solutions are not available or very expensive. Therefore, it is a good idea to look at the open-source communities to see what's around.

Idea Summary

We load Jasmine in one web page that has an iframe. We load the page that we would like to test inside that iframe. We run test cases to test our page. Finally, we capture the results using Selenium to integrate it with Microsoft Test suite (or NUnit, whatever server-side testing).

Why Jasmine + Selenium

There are so many JavaScript and Web UI testing frameworks. We considered the following list for our testing purposes:

  • Microsoft Coded UI
  • Telerik HTML test suite
  • HP QuickTest
  • HTML5Robot
  • Cogitek RIATest
  • WatiN
  • Bryntum Siesta
  • Chutzpah
  • Qunit

Here, I am going to explain why I preferred to use Jasmine and Selenuim over the above list for my project.

  • Telerik HTML test suite, HP QuickTest and Cogitek RIATest excluded because of their commercial licenses. We didn't want to pay extra fees for testing purposes, so we excluded all commercial solutions. You may consider them for your project, though.
  • Microsoft Coded UI was not a good option because of two reasons: first, it doesn't allow real JavaScript testing; second, it captures everything like a recorder that seems to be unmaintainble. A simple testing UI has a thousand lines of generated code in this tool.
  • Bryntum Siesta seems to be good only for development of client-side extjs applications. It has a nice web user interface, but it doesn't come with a text-based UI. So, it is hard to be integrated with other solutions like Selenium.
  • HTML5Robot is not mature enough yet. Like many other solutions, it is not possible to be easily integrated with the development life cycle. In addition, it doesn't allow js testing. Nevertheless, I really liked its human readable generated tests.
  • WatiN is good, and it is very similar to Selenium, but it only works with Internet Explorer. I wanted to have the option of automated testing in Firefox and Chrome that was impossible with WatiN.
  • Chutzpah is awesome for testing JavaScript libraries. Its major downside for our project was that it couldn't test a real web page. By the time I am writing this, it executes scripts in a client side HTML file that even makes it impossible to test a page in Iframe in a real server because of browsers' security issues. In my opinion, it is, however, the best available .NET integrated solution for testing standalone js libraries.
  • Qunit is similar to Jasmine. It is very simple to use. However, many developers prefer Jasmine because of its BDD style (behavior-driven development). So, I decided to go with Jasmine.

To test JavaScript and web user interface, we decided to use Jasmine and we used Selenium to integrate that Jasmine with MS Test in our project.

Note: These are not the only available options. There are libraries like Mocha,... that can be considered for other projects. And this approach is not the only approach to test ASP.NET UI. You may use other approaches or invent your own. I just wanted to share my own way with others.

Note 2: Items of this list are not similar in functions they provide. For example, QUnit provides only JavaScript Testing and Microsoft coded UI provides tests using recorded actions from the browser. Therefore, being in this list doesn't mean to be in the same category.

Overview

Our program has two major parts:

  • JavaScript part
  • .NET part

JavaScript Part

The main purpose of this part is to test UI using a JavaScript testing framework. It doesn't use any ASP.NET based technology to perform; so, it can be easily used in other web projects such as PHP or Java.
We use the following files in for JavaScript testing part:

  • JsTest.aspx: This file executes our test cases.
  • JsTestAll.aspx: This file executes all test cases together. It runs JsTest file several times inside different iframes.
  • TestFilesInfo.js: This file contains information about test case files.
  • TestUtils.js: Some test utilities can be used in many test cases. Functions provided by this class is like clicking on a button or generating a random number.
  • Other js files: They are written test cases that are different from project to project.

.NET Part

The purpose of this part is to integrate our JavaScript tests with .NET test suites such as Microsoft Test or NUnit. It uses Selenium to perform automated testing. In fact, this part is not necessary. It only helps to do final testing inside Visual Studio using its Test Explorer features. It can also help to automate executing of tests in different browsers because of Selenium.
We are using the following classes in this part:

  • SeleniumBrowserTestBase.cs: This class provides basic methods required to define a Selenium test case. This is an abstract class.
  • JasmineTestPageBase.cs: This is an abstract class to capture results of a Jasmine execution. We define our test cases by inheriting from this module.
  • Other cs files: They are test case execution files.

Setup Jasmine

Jasmine is a JavaScript test suite. It helps to develop JavaScript tests easier. Using this library is pretty easy. The only thing you need to do is to add it to your testing page.

At first, Jasmine files should be copied to the website.

Then, we need to include its js files in our JsTest.aspx file. So, we include these lines in the header of JsTest.aspx.

ASP.NET
<!-- ------- Jasmine Test suite required files ----------- -->
<link rel="shortcut icon" type="image/png" href="jasmine-2.0.0/jasmine_favicon.png" />
<link rel="stylesheet" type="text/css" href="jasmine-2.0.0/jasmine.css" />
<script type="text/JavaScript" src="jasmine-2.0.0/jasmine.js"></script>
<script type="text/JavaScript" src="jasmine-2.0.0/jasmine-html.js"></script>
<script type="text/JavaScript" src="jasmine-2.0.0/boot.js"></script>
<!-- ------- End Jasmine Test suite required files ---------------- -->

Setting up Jasmine is as simple as this. I supposed that you know how to use Jasmine, so I didn't explain much about it here. For more information, you can check Jasmine documentation.

Simple JavaScript Test

Now it's time to test a simple JavaScript file. The first thing that I would like to do is to control Jasmine execution by my code. So, the next thing is to modify Jasmine boot.js file and remove its environment execution.

JavaScript
window.onload = function() {
if (currentWindowOnload) {
    currentWindowOnload();
}
htmlReporter.initialize();
//env.execute();
};

I just removed //env.execute(); from the file.

DeltaCompress.js is a file that I would like to test it. This file has several functions that I should test first. The file looks like this:

JavaScript
var DeltaCompress = {

    compArray: function (arr, findex) {
        if (DeltaCompress.preCheckArr(arr) == false)
            return arr;
        var tmp = 0;
        var prev = arr[0];
        for (var i = 1; i < arr.length; i++) {
            tmp = arr[i];
            arr[i] = arr[i] - prev;
            prev = tmp;
        }
        return arr;
    },
    decompArray: function (arr) {
        if (DeltaCompress.preCheckArr(arr) == false)
            return arr;
        for (var i = 1; i < arr.length; i++) {
            arr[i] = arr[i] + arr[i-1];
        }
        return arr;
    },
    //...
} 

We don't care what DeltaCompress does here, but if you are really curious to know, I try to explain it in one line: DeltaCompress can be used to decompress numeric Json Arrays. Suppose that I have a very large number like 1147651200000, and the next number is 1147737600000. String Len(1147651200000) + Len(1147737600000) = 26. As len(1147737600000 - 1147651200000 = 86400000) = 8, I can save 10 characters per two numbers in transferring data from the server to client and back. So, DeltaCompress replaces a number with its distance from its previous number.

To test this file, I create another JavaScript file called DeltaCompressTests.js, and I add my Jasmine test cases to this file:

JavaScript
describe("DeltaCompress.compArrayDecompArray(Arr)", function () {
     it("passes if it compresses and decompresses an array successfully", function () {
         var a = GetArray();
         var aCopy = GetArray();
         DeltaCompress.compArray(aCopy);
         DeltaCompress.decompArray(aCopy);
         for(var i =0; i< a.length; i++)
             expect(a[i]).toEqual(aCopy[i]);
     });
 });
 describe("DeltaCompress.compArrayObjDecompArrayObj(Arr, pInx)", function () {
     it("passes if it compresses and decompresses an array of objects successfully", function () {
         var a = GetArrayObj();
         var aCopy = GetArrayObj();
         var pInx = 0;
         DeltaCompress.compArrayObj(aCopy, pInx);
         DeltaCompress.decompArrayObj(aCopy, pInx);
         for (var i = 0; i < a.length; i++)
             expect(a[i][pInx]).toEqual(aCopy[i][pInx]);
     });
 });

 function GetArray() {
     return [517, 511, 508, 1023, 507, 507, 506, 1022, 505, 505, 505,
     505, 505, 505, 505, 505, 506, 506, 506, 1022, 507, 507, 507, 508,
     507, 507, 507, 1023, 507, 508, 509, 509, 510, 510, 510, 1022, 510,
     510, 511, 1023, 511, 512, 512, 513, 513, 514, 515, 1023, 516, 517,
     518, 1023, 519, 520, 520, 520, 521, 521, 521, 520, 520, 519, 519,
     518, 518, 517, 1023, 514, 512, 511, 509, 508, 506, 505, 504, 504,
     503, 503, 1022, 502, 502, 502, 1022, 502, 502, 502, 1022, 502, 502,
     502, 1022, 502, 503, 503, 1023, 503, 503, 503, 1023, 503, 503, 504,
     504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504,
     504, 504, 504, 504, 504, 504, 504, 504, 504, 505, 505, 504, 504, 504,
     504, 504, 504, 504, 504, 504, 504, 505, 505, 504, 504, 1023, 503, 504,
     504, 505, 505, 505, 505, 505, 505, 505, 505, 505, 505, 505, 505, 505,
     505, 505, 505, 505, 505, 505, 505, 505, 505, 506, 506, 1022, 506, 506,
     506, 1022, 507, 507, 507, 508, 508, 508, 508, 1023, 507, 507, 507,
     1023, 507, 506, 506, 506, 506, 506, 1022, 506, 506, 506, 1022, 506,
     506, 506, 1022, 506, 506, 506, 1022, 506, 506, 505, 1023, 501, 501, 505];
 }

 function GetArrayObj() {
     return [
             /* May 2006 */
             [1147651200000, 67.79],
             [1147737600000, 64.98],
             [1147824000000, 65.26],
             [1147910400000, 63.18],
             [1147996800000, 64.51],
             [1148256000000, 63.38],
             [1148342400000, 63.15],
             [1148428800000, 63.34],
             [1148515200000, 64.33],
             [1148601600000, 63.55],
             [1148947200000, 61.22],
             [1149033600000, 59.77],
             ...
     ];
 }

 TestSimpleJavaScriptFile("../js/DeltaCompress.js");

Note: Please note that these test cases are sample test cases, and they are not sufficient to test the whole library. I just put two test cases to show how it works.

"Describe" functions are simple Jasmine test functions. They should be easy to understand, so I avoid more explanations here. Last line of this js file is something that I would like to use to start execution of the test TestSimpleJavaScriptFile("../js/DeltaCompress.js");. TestSimpleJavaScriptFile is a function in our JsTest.aspx file that I should write its body there.

Since we should load our test JavaScript function in JsTest.aspx, I should add more functions to JsTest to load this JavaScript file and start testing:

/*
    Tests a stand-alone js file. It is not required to be in a specific page
*/
function TestSimpleJavaScriptFile(filename) {
    LoadJavaScriptFile(filename);
    window.onload = function () {
        if (currentWindowOnload) {
            currentWindowOnload();
        }
        ExecuteJasmine(window);
    }
}

/*
    * Loads a js file into the current page
*/
function LoadJavaScriptFile(filename) {
    var fileref = document.createElement('script');
    fileref.setAttribute("type", "text/JavaScript");
    fileref.setAttribute("src", filename);
    document.getElementsByTagName("head")[0].appendChild(fileref);
}

/*
    * Executes Jasmine environment to run test cases.
    * If there is a client-side framework like Ext, it waits for that framework
*/
function ExecuteJasmine(TestPage)
{
    jasmine.getEnv().execute();
}

I would like to run my test classes like this: JsTest.aspx?TestCase={JsPath} where {JsPath} can be any JavaScript test class like DeltaCompressTests.js; therefore, I need to load this test file to JsTest.aspx. Here is the server-side code:

C#
protected void Page_Load(object sender, EventArgs e)
{
    if (Request.QueryString["TestCase"] != null)
    {
        string testPath = Request.QueryString["TestCase"];
        Page.ClientScript.RegisterClientScriptInclude
        ("TestCase", Page.ResolveClientUrl("~/TestCases/" + testPath));
    }
}

This code does nothing but adding a JavaScript test file to the page; it is simple enough to be converted to any server-side language like PHP or Java. It can also be implemented in JavaScript! So, no server-side code is really required here, but let's keep it like this. I think that you got the idea that we just load our test class to the page, and the test class loads the file under test and executes Jasmine environment to run tests.

Now I can run my test page like this: JSTest.aspx?TestCase=js/DeltaCompressTests.js to see the results:

Jasmine execution results in an ASP.NET page testing a simple JavaScript library

We finished our first happy scenario just now! We integrated our JavaScript test files to our ASP.NET application. They can now be simply a part of our source control in the development process. We can use Visual Studio debugger to test our Jasmine test cases as well. Next section shows how can we test a JavaScript under a real page.

Testing a Web Page

I am not happy with my achievements so far. Therefore, I would like to expand my code to add functionality of loading a web page and testing it. In addition, There are so many JavaScript functions that can't be tested without a web page. For example, I would like to test a JS function that gets query string.

In this section, I have a Common.js file that has so many common functions that can be useful for many pages in my web application. The following is a few lines of this file:

JavaScript
var Framework = {

/**
* gets a value from query string
*/
getQueryString: function (urlVarName) {
    var urlHalves = String(document.location).split('?');
    var urlVarValue = '';
    if (urlHalves[1]) { var urlVars = urlHalves[1].split('&');
    for (i = 0; i <= (urlVars.length); i++) { if (urlVars[i])
    { var urlVarPair = urlVars[i].split('=');
    if (urlVarPair[0] && urlVarPair[1] && urlVarPair[0] == urlVarName)
    { urlVarValue = urlVarPair[1]; } } } }
    return urlVarValue;
},

//...

In order to test this file, I need to add another test file to my TestCase folder named fwTests.js. This file is similar to my previous test class, but it comes with some differences:

JavaScript
var fwTests = {

    StartTest : function()
    {
        describe("Framework Sanity", function () {
            it('passes if all variables are defined', function () {
                expect(TestPage.Framework).toBeDefined();
            });
        });

        describe("Framework.getQueryString(TestMode)", function () {
            it("passes if test mode was True", function () {
                var expected = TestPage.Framework.getQueryString("TestMode");
                expect(expected).toContain("True");
            });
        });

    //...
    }
}

// loads the test page and start testing
LoadTestPage({
    pageUrl: "../Default.aspx?TestMode=True",
    startTestFunction: fwTests.StartTest
});

Here is the list of differences:

  • fwTests class: I put my test functions infwTests object (class). It has two advantages:
    1. Jasmine Runtime can't find them before I call StartTest.
    2. Stupid Chutzpah can't find my test cases. In fact, it shouldn't find them because it can't run them!
  • StartTest function: Test cases will be captured by Jasmine when StartTest is called. In this way, I can start tests when all my JavaScript frameworks such as ExtJs or JQuery are loaded and page is ready for test with minimum modification of Jasmine boot.js file (remember that we already did that modification in the setup section of this document). So, it helps me to keep my test execution clean.
  • LoadTestPage function: LoadTestPage is again a function in my JsTest.aspx file that I should develop its body. I really like JavaScript way of running function when I can define an object with any properties as an input to that function. It keeps the code readable and understandable.
  • TestPage variable: If you look carefully, you can see running Framework functions are started with TestPage.Framework instead of Framework. It is because Framework is inaccessible in our JSTest file. JsTest loads our test page (in this case Default.aspx) in an iframe that its content is accessible using TestPage variable.

Now it's time to change my JsTest.aspx to load my test page and execute test cases. Since I would like to keep each test class in an isolated environment, I load a test page in an Iframe, and put its reference to a variable named TestPage.

JavaScript
var TestPage = null;  // keeps TestPage window. Test cases use this variable to do testing
var currentWindowOnload = window.onload; // Keeps window.onLoad function that is currently set. Jasmine boot file set a function for this event by default.
var isWindowLoaded = false; // shows if this window is loaded or not.
//It is because we would like to use window.onload once in our application
var isJasmineExecuted = false; // keeps the execution status of Jasmine.
//When environment executed, it becomes true.
//It is because we don't want to execute environment several times :)
var defaultIframeWidth = 800; // default value for test page iframe width
var defaultIframeHeight = 500; // default value for test page iframe height
/*
    Loads a page for test
    e.pageUrl: the Url for testing
    e.startTestFunction: reference to the startTest function
    e.height: height of test page iframe. If not set, it uses a default value
    e.width: width of test page iframe. If not set, it uses a default value
*/
function LoadTestPage(e) {
    // In Firefox, iframe can be loaded before the main page
    // So, tests could be run before jasmine htmlreporter be created
    // This causes problems. So, we need to do everything after load
    if (isWindowLoaded == false)
    {
        window.onload = function () {
            if (currentWindowOnload) {
                currentWindowOnload();
            }
            __LoadTestPageIframe(e);
        }
        isWindowLoaded = true;
    }
    else
        __LoadTestPageIframe(e);
}
function __LoadTestPageIframe(e)
{
    pageUrl = e.pageUrl;
    startTestFunction = e.startTestFunction;
    height = e.height ? e.height : defaultIframeHeight;
    width = e.width ? e.width : defaultIframeWidth;
    //if (e.useExistingIframe)
    //    iframe = __LoadIframeUrl(pageUrl, height, width);
    //else
        iframe = __CreateNewIframeUrl(pageUrl, height, width);
    // once iFrame loaded, we need to load test cases and execute them
    iframe.onload = function () {
        TestPage = iframe.contentWindow;
        // We call start test function to load all available test cases in Jasmine
        // Test cases won't run before Jasmine execution
        startTestFunction();
        // Executing Jasmine environment
        ExecuteJasmine(TestPage);
        // end Executing Jasmine environment
    }
}
/*
    * Executes Jasmine environment to run test cases.
    * If there is a client-side framework like Ext, it waits for that framework
*/
function ExecuteJasmine(TestPage)
{
    if (isJasmineExecuted == false) {
        jasmine.getEnv().execute();
        isJasmineExecuted = true;
    }
}

/*
    A variable to create unique ids for dynamically created iframes
*/
var iframeNumber = 0;
/*
    * Creates a new dynamic iframe and appends it to the document body
    If you are changing this method be sure that it is compatible with JSTestAll file
*/
function __CreateNewIframeUrl(pageUrl, height, width) {
    iframe = document.createElement('iframe');
    iframe.id = "mainIFrame" + iframeNumber;
    iframe.width = width;
    iframe.height = height;
    iframe.src = pageUrl;
    iframeNumber++;
    document.body.appendChild(iframe);
    return iframe;
}

Now, I should include my Common.js in Default.aspx file:

JavaScript
<script type="text/JavaScript" src="FW/js/Common.js"></script>

Please note that Default.aspx can be any page in our website. It can include any number of JavaScript with a complex user interface. This is just a very simple example page. It's time to run my test cases in a real page using this url: JSTest.aspx?TestCase=FW/fwTests.js

Jasmine execution results for FWTest

ExtJs Integration

This example shows how to test the page when it contains a dynamic control creation library like ExtJs. I just need to change ExecuteJasmine function to add support for Ext. It executes Jasmine after Ext framework is ready (The same thing can be done for JQuery library). So, final ExecuteJasmine function looks like this:

JavaScript
/*
     Executes Jasmine environment to run test cases.
     If there is a client-side framework like Ext, it waits for that framework
 */
 function ExecuteJasmine(TestPage)
 {
     if (isJasmineExecuted == false) {
         // if Ext existed in the page, let it be ready, otherwise run current tests
         // if you have any other JS framework such as JQuery,
         // you need to add it to this part to make sure that
         // Jasmine executes after used frameworks are loaded
         if (TestPage.Ext) {
             TestPage.Ext.onReady(function () {
                 jasmine.getEnv().execute();
             });
         }
         else {
             jasmine.getEnv().execute();
         }
         isJasmineExecuted = true;
     }
 }

Async Function Testing

So many web activities, specially in the user interface, are asynchronous today. We have a simple test scenario: we show a confirm delete dialog to the user; if he clicked on it, we should check if delete function is called or not. So, we have a confirm function in our Common.js like this:

JavaScript
/**
* Asks user to confirm deleting an item before delete
*/
confirmDelete: function (deleteFunction, returnControl) {
    var msgSettings = {
        title: StringMsgs.Framework.ConfirmDelete_DialogTitle,
        msg: StringMsgs.Framework.ConfirmDelete_Msg,
        buttons: Ext.Msg.YESNO,
        fn: function (btn) {
            if (btn == 'yes')
                deleteFunction();
            if (returnControl) // focus for the control after execution
                if (returnControl.focus !== undefined)
                    returnControl.focus();
        }
    }
    if (returnControl)
        msgSettings.animEl = returnControl.id;
    return Ext.Msg.show(msgSettings);
},

By the time I am writing this article, Jasmine 2.0.0 can do async tests, but this feature is not documented yet. Here is how to do it. So, we develop the test case like this:

JavaScript
 describe("Framework.confirmDelete(yes)", function () {
    it("passes when it doesn't calls delete function successfully",
        function (done) { // magic. If you add done here, it can handle async :)
        spyOn(fwTests, 'deleteFunction');
        var msgWindow = TestPage.Framework.confirmDelete(fwTests.deleteFunction);
        setTimeout(function () { // wait for msg to be created
            var btn = ExtTestUtils.messageBoxButtonClick(msgWindow, "yes");
        }, 100);
        setTimeout(function () {
            expect(fwTests.deleteFunction).toHaveBeenCalled();
            done();
        }, 1000);
    });
});

// fake delete function!
deleteFunction : function() {
    fwTests.isDeleteFunctionCalled = true;
},

To make Jasmine do an async test, we just need to put done variable in function definition and call it once the test passed. We also put a spy on our fake delete function to make sure that it will be called when the user clicks on the Yes button. setTimeout is used to let the browser finish its activity.

The same approach can be used to test any Async activity like Ajax.

Testing One Page with Different Query String Parameters

It is simple to test a page using different query string parameters in JSTest.aspx. The only thing you need to do is to use Async test of Jasmine and call LoadTestPage function inside the test case. In this example, we would like to create a fake test page to test an abstract ASP.NET class.

Suppose that our website pages get some queryString parameters and create some classes. Since many pages need this function, we would like to create it in a base class called BasePage. This C# code shows this function:

C#
public abstract class EntityBase
{
    public abstract string ClassName { get;}
}

public class UserEntity : EntityBase
{
    public override string ClassName
    {
        get { return "UserClass"; }
    }
}

public class RoleEntity : EntityBase
{
    public override string ClassName
    {
        get { return "RoleClass"; }
    }
}

/// <summary>
/// Summary description for BasePage
/// This is a base class for some of the pages in the application
/// This have the same function that they check query string for a value and if it was there, they create an object using it.
/// I just put it here to show that how an abstract class can be tested using fake pages in our JSTest.aspx file
/// </summary>
public class BasePage : System.Web.UI.Page
{
    public EntityBase Entity { get; set; }
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        // It is a very simple computation from QueryString, but it can be very complicated in a real application
        // It just checks QueryString, and if Entity was there, it creates an object using this parameter
        string entity = Request.QueryString["Entity"];
        if (string.IsNullOrEmpty(entity) == false)
        {
            switch (entity)
            {
                case "User":
                    this.Entity = new UserEntity();
                    break;
                case "Role":
                    this.Entity = new RoleEntity();
                    break;
                default:
                    break;
            }
        }
    }
}    

In order to test BasePage class, we need to create a fake class in our TestCase folder. Let's call it BasePageFake, and write this in its body:

C#
public partial class TestCases_FW_BasePageFake : BasePage
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (this.Entity != null)
        {
            this.divEntityName.InnerText = this.Entity.ClassName;
        }
    }
}

To test this page, we can create another JavaScript test class. Let's call it BasePageTests.js and write this in it:

var BasePageTests = {
    StartTest: function () {
        describe("BasePageFake with User parameter", function () {
            it("passes when it creates all parameters",
                function (done) { // magic. If you add done here it can handle async :)
                    LoadTestPage({
                        pageUrl: "FW/BasePageFake.aspx?Entity=User",
                        startTestFunction: function () {
                            expect(TestPage.divEntityName.innerText).toEqual("UserClass");
                            done();
                        },
                        height: 100,
                        width: 300
                    });
            });
        });
        describe("BasePageFake with Role parameter", function () {
            it("passes when it creates all parameters",
                function (done) { // magic. If you add done here it can handle async :)
                    LoadTestPage({
                        pageUrl: "FW/BasePageFake.aspx?Entity=Role",
                        startTestFunction: function () {
                            expect(TestPage.divEntityName.innerText).toEqual("RoleClass");
                            done();
                        },
                        height: 100,
                        width: 300
                    });
            });
        });
    }
}
// loads the test page and start testing
LoadTestPage({
    pageUrl: "FW/BasePageFake.aspx",
    startTestFunction: BasePageTests.StartTest,
    height: 200,
    width: 300
});

As you can see in the above code, we can test the same page in one test class (js file). Here we are testing server-side code using our JavaScript testing framework.

Jasmine execution results for BasePageFake page

Showing Test Classes in JsTest File

It is hard to use JSTest.aspx?TestCase={TestPath} for each test class; isn't it? Programmers are notorious for being lazy! So, I would like to show list of my test classes in JSTest if TestCase parameter was not available. Thus, I add TestFilesInfo.js with the following code:

/*
  Keeps information about test case files
*/
var TestFilesInfo = {
    /*
      returns a list of test case files
    */
    GetTestList: function () {
        if (TestFilesInfo.TestList)
            return TestFilesInfo.TestList;
        else
        {
            var list = [];
            list.push("FW/fwTests.js"); // a JavaScript that should be tested in a page
            list.push("js/DeltaCompressTests.js"); // a stand-alone js file (it can also be tested using chotzpah)
            list.push("FW/BasePageTests.js"); // a JavaScript to test abstract asp.net page classes
            TestFilesInfo.TestList = list;
            return TestFilesInfo.TestList;
        }
    }
}

Then I modify my JSTest class like this to call CreateTestList function if TestCase was not provided in QueryString:

C#
protected void Page_Load(object sender, EventArgs e)
{
    if (Request.QueryString["TestCase"] != null)
    {
        string testPath = Request.QueryString["TestCase"];
        Page.ClientScript.RegisterClientScriptInclude("TestCase", Page.ResolveClientUrl("~/TestCases/" + testPath));
    }
    else
    {
        Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "Test", "CreateTestList();",true);
    }
}

And adding this to its JavaScript side:

JavaScript
////////////////////////////////////////////////////////////
// Functions for when no test case provided in QueryString
////////////////////////////////////////////////////////////
/*
    * Creates a link to a test case file
*/
function __CreateTestTitleElement(url, title) {
    var div = document.createElement("div");
    div.innerHTML = "<p><a href='" + url +
    "' target='_blank'>" + title + "</a></p>";
    document.body.appendChild(div);
}
/*
    * Creates a list of test cases using TestFilesInfo file
*/
function CreateTestList() {
    var testList = TestFilesInfo.GetTestList();
    for (var i = 0; i < testList.length; i++) {
        var testUrl = testList[i];
        var url = "JSTest.aspx?TestCase=" + testUrl;
        __CreateTestTitleElement(url, testUrl);
    }
}

Now, it can run CreateTestList function when TestCase parameter is not in QueryString, and creates a list of test classes.

All test case list in JSTest page

Running All JavaScript Tests at Once

I think that it is still hard to test all test classes one by one. In addition, before each release (or check-in), I would like to test all my test classes. So, I need to create another test page that runs all my test cases at once.

JavaScript
/*
     Executes all available test cases
 */
 function ExecuteAllTests()
 {
     var testList = TestFilesInfo.GetTestList();
     for (var i = 0; i < testList.length; i++)
     {
         ExecuteTest(testList[i]);
     }
 }
 /*
     Executes a single test suite file using its Url
 */
 function ExecuteTest(testUrl)
 {
     var url = "JSTest.aspx?TestCase=" + testUrl;
     var title = testUrl.replace("/", " ");
     title = title.replace(".js", "");
     __CreateTestTitleElement(url, title);
     __CreateNewIframeUrl(url, 100, 800);
 }

The results should be something like this:

Jasmine execution results for all test case classes in JSTestAll page

It is good enough for a small project with a few test classes.

Integration with Selenuim (.NET Part!)

Selenuim is one of the most popular web UI testing frameworks. Although I would like to do all my tests in the client-side using JavaScript, it doesn't harm if I integrate it with Selenium. It can simplify test execution in different browsers and be used as a part of continuous integration.

I add a new test project to my solution, and add Selenuim reference to it. This should be a separate project from the current server-side code test project.

I suppose that you know how to run a Selenuim test, so I don't explain it here. At first, I create a base class for my Selenium Test cases, and I call it BaseBrowserTest:

C#
public class SeleniumBrowserTestBase
{
    protected static IWebDriver driver;
    protected StringBuilder verificationErrors;
    protected string baseURL;
    public SeleniumBrowserTestBase()
    {
        this.baseURL = "http://localhost:46174";
    }
    [TestInitialize()]
    public void SetupTest()
    {
        if (driver == null)
            InitializeBrowserDriver();
        //baseURL = "https://www.google.com/";
        verificationErrors = new StringBuilder();
    }
    private void InitializeBrowserDriver()
    {
        driver = new FirefoxDriver();
        //driver = new ChromeDriver();
        //driver = new InternetExplorerDriver();
    }
    [TestCleanup()]
    public void TeardownTest()
    {
        try
        {
            // we don't want to close browser as there might be several other test cases to use the browser
            //driver.Quit();
        }
        catch (Exception)
        {
            // Ignore errors if unable to close the browser
        }
        Assert.AreEqual("", verificationErrors.ToString());
    }
//...
}

Note that baseURL should contain the base URL for the ASP.NET project. In fact, the website should be accessible before running test cases. We can also put this value in a config file later.

Later, I create another class that can check Jasmine results with Selenuim functions:

C#
    /// <summary>
/// Base page for Jasmine test suite
/// </summary>
public abstract class JasmineTestPageBase : SeleniumBrowserTestBase
{
    /// <summary>
    /// Gets the name of the java script test file.
    /// </summary>
    /// <value>
    /// The name of the java script test file.
    /// </value>
    public string JavaScriptTestFileName { get; set; }
    private int waitingTimeSeconds = 6;
    /// <summary>
    /// Gets or sets the waiting time seconds.
    /// </summary>
    /// <value>
    /// The waiting time seconds.
    /// </value>
    public int WaitingTimeSeconds
    {
        get { return waitingTimeSeconds; }
        set { waitingTimeSeconds = value; }
    }
    public void RunJasmineTest()
    {
        driver.Navigate().GoToUrl(baseURL + "/TestCases/JSTest.aspx?TestCase=" + this.JavaScriptTestFileName);
        //IJavaScriptExecutor js = (IJavaScriptExecutor)driver;
        //js.ExecuteScript("FrameworkTest.getQueryString();");
        for (int second = 0; ; second++)
        {
            if (second >= this.waitingTimeSeconds * 10) Assert.Fail("timeout");
            try
            {
                var element = driver.FindElement(By.CssSelector("span.duration"));
                if (element != null)
                {
                    var duration = element.Text;
                    if (duration.Contains("finished in"))
                        break;
                }
            }
            catch (Exception)
            { }
            System.Threading.Thread.Sleep(100);
        }
        //x specs, 0 failures
        string results = "";
        try
        {
            results = driver.FindElement(By.CssSelector("span.bar.passed")).Text;
        }
        catch(NoSuchElementException) // test probably failed
        {
            results = driver.FindElement(By.CssSelector("span.bar.failed")).Text;
            throw new Exception("Test failed: " + results);
        }
        Assert.AreEqual(results.Contains("0 failures"), true);
    }
}

Unfortunately, one of the problems with testing UI using Selenuim is managing execution times. This code tries to get results every 100 milliseconds. Test will fail if it couldn't be finished in pre-defined timeout value in waitingTimeSeconds variable.

Finally, I create a simple class to test one of my JavaScript test classes:

C#
[TestClass()]
public class FWTests : JasmineTestPageBase
{
    public FWTests()
    {
        //baseURL = "http://localhost:7255";
        this.JavaScriptTestFileName = "FW/fwTests.js";
        this.WaitingTimeSeconds = 20;
    }
    [TestMethod()]
    public void RunFWTests()
    {
        RunJasmineTest();
    }
}

The only thing I need to do is to specify JavaScript file name and time-out time. My test method just runs Jasmine by calling RunJasmineTest();

In this example, we have used FireFox driver to test our pages in Firefox, but we can use other drivers like Chrome to test under other browsers. In addition, we can change the code in a way to run the same test classes under different browsers. Since it was a small test project, I didn't want to make it more complicated.

Author's Words

It is my first CodeProject Article. The fun fact is that writing this article took more time than developing the original code. :) I did my best to provide something useful for you, and I hope that you like it. I really appreciate your comments and suggestions. All provided code is free to use for any type of application without any requirements.

Points of Interest

By choosing Jasmine and Selenium, not only did I fulfill the current needs for testing web UI, but I also got some other side benefits:

  • It is completely integrated to our development process. It made test-driven development much easier for our JavaScript tasks.
  • In addition, we can test our UI in the real customer site without installing any other application! (Just keep TestCase folder)
  • Finally, we are now able to test our site in mobile device browsers like tablets and smart phones without using any other apps. (Just run JSTestAll.aspx there!)

History

  • 15th March, 2014: First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
Dr. Ghaderi started software development when he was 13. Visual Basic was his first programming language. He is knowledgeable of many programming languages such as C#, C/C++, JavaScript, TypeScript, Java, ActionScript, PHP, and Python. He started web development in 2005 using ASP.NET and C#. He has developed a variety of software solutions such as Management Information Systems, ERPs, E-commerce apps, etc. He is interested in topics related to software research and development.

Comments and Discussions

 
GeneralGood article and informative Pin
Jaison Peter19-Jul-17 22:06
Jaison Peter19-Jul-17 22:06 
GeneralRe: Good article and informative Pin
mohghaderi20-Mar-18 15:29
professionalmohghaderi20-Mar-18 15:29 
QuestionGood Work! Pin
Your Display Name Here3-Apr-15 11:38
Your Display Name Here3-Apr-15 11:38 
AnswerRe: Good Work! Pin
mohghaderi20-Mar-18 15:31
professionalmohghaderi20-Mar-18 15:31 
QuestionTest the simple apps of SharePoint2013 like creating a list using jasmine framework only. Pin
Member 1109149522-Sep-14 0:57
Member 1109149522-Sep-14 0:57 
QuestionSiesta Pin
Member 1067531716-Mar-14 19:12
Member 1067531716-Mar-14 19:12 
AnswerRe: Siesta Pin
mohghaderi20-Mar-18 15:33
professionalmohghaderi20-Mar-18 15:33 
GeneralMy vote of 5 Pin
Marc Clifton16-Mar-14 1:57
mvaMarc Clifton16-Mar-14 1:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.