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.
<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>
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.
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
htmlReporter.initialize();
};
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:
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:
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 [
[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:
function TestSimpleJavaScriptFile(filename) {
LoadJavaScriptFile(filename);
window.onload = function () {
if (currentWindowOnload) {
currentWindowOnload();
}
ExecuteJasmine(window);
}
}
function LoadJavaScriptFile(filename) {
var fileref = document.createElement('script');
fileref.setAttribute("type", "text/JavaScript");
fileref.setAttribute("src", filename);
document.getElementsByTagName("head")[0].appendChild(fileref);
}
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:
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:
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:
var Framework = {
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:
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");
});
});
}
}
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: - Jasmine Runtime can't find them before I call
StartTest
. - 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
.
var TestPage = null;
var currentWindowOnload = window.onload;
var isWindowLoaded = false;
var isJasmineExecuted = false;
var defaultIframeWidth = 800;
var defaultIframeHeight = 500;
function LoadTestPage(e) {
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;
iframe = __CreateNewIframeUrl(pageUrl, height, width);
iframe.onload = function () {
TestPage = iframe.contentWindow;
startTestFunction();
ExecuteJasmine(TestPage);
}
}
function ExecuteJasmine(TestPage)
{
if (isJasmineExecuted == false) {
jasmine.getEnv().execute();
isJasmineExecuted = true;
}
}
var iframeNumber = 0;
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:
<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
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:
function ExecuteJasmine(TestPage)
{
if (isJasmineExecuted == false) {
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:
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)
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:
describe("Framework.confirmDelete(yes)", function () {
it("passes when it doesn't calls delete function successfully",
function (done) {
spyOn(fwTests, 'deleteFunction');
var msgWindow = TestPage.Framework.confirmDelete(fwTests.deleteFunction);
setTimeout(function () {
var btn = ExtTestUtils.messageBoxButtonClick(msgWindow, "yes");
}, 100);
setTimeout(function () {
expect(fwTests.deleteFunction).toHaveBeenCalled();
done();
}, 1000);
});
});
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:
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"; }
}
}
public class BasePage : System.Web.UI.Page
{
public EntityBase Entity { get; set; }
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
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:
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) {
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) {
LoadTestPage({
pageUrl: "FW/BasePageFake.aspx?Entity=Role",
startTestFunction: function () {
expect(TestPage.divEntityName.innerText).toEqual("RoleClass");
done();
},
height: 100,
width: 300
});
});
});
}
}
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.
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:
var TestFilesInfo = {
GetTestList: function () {
if (TestFilesInfo.TestList)
return TestFilesInfo.TestList;
else
{
var list = [];
list.push("FW/fwTests.js");
list.push("js/DeltaCompressTests.js");
list.push("FW/BasePageTests.js");
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
:
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:
function __CreateTestTitleElement(url, title) {
var div = document.createElement("div");
div.innerHTML = "<p><a href='" + url +
"' target='_blank'>" + title + "</a></p>";
document.body.appendChild(div);
}
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.
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.
function ExecuteAllTests()
{
var testList = TestFilesInfo.GetTestList();
for (var i = 0; i < testList.length; i++)
{
ExecuteTest(testList[i]);
}
}
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:
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
:
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();
verificationErrors = new StringBuilder();
}
private void InitializeBrowserDriver()
{
driver = new FirefoxDriver();
}
[TestCleanup()]
public void TeardownTest()
{
try
{
}
catch (Exception)
{
}
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:
public abstract class JasmineTestPageBase : SeleniumBrowserTestBase
{
public string JavaScriptTestFileName { get; set; }
private int waitingTimeSeconds = 6;
public int WaitingTimeSeconds
{
get { return waitingTimeSeconds; }
set { waitingTimeSeconds = value; }
}
public void RunJasmineTest()
{
driver.Navigate().GoToUrl(baseURL + "/TestCases/JSTest.aspx?TestCase=" + this.JavaScriptTestFileName);
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);
}
string results = "";
try
{
results = driver.FindElement(By.CssSelector("span.bar.passed")).Text;
}
catch(NoSuchElementException)
{
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:
[TestClass()]
public class FWTests : JasmineTestPageBase
{
public FWTests()
{
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
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.