Click here to Skip to main content
15,890,579 members
Articles / Programming Languages / Javascript
Article

JavaScript Based Automated Testing for Web Applications

Rate me:
Please Sign up or sign in to vote.
4.33/5 (2 votes)
25 Feb 2011CPOL4 min read 32.5K   379   13  
The idea was to create a wrapper parent HTML page which will 'host' the web-page to be tested in an iframe and then access its controls, populate value and perform clicks, etc...

Background

I've been looking for auto-testing tools for my web-app developed in C# having AJAX. I tried to find some tools but some were complex, some were paid and some would not allow me to tweak in. Some are listed below but I wanted something on which I had the total control and would be very lightweight and easily portable.

My inspirations:

  • http://watin.sourceforge.net
  • http://sahi.co.in/w/
  • http://seleniumhq.org/projects/ide/

Solution

So, finally getting back to my old JavaScript skills I ended up creating my own simple (and hopefully effective) auto-testing tool! The idea was to create a wrapper parent HTML page which will 'host' the web-page to be tested in an iframe and then access its controls, populate value and perform clicks, etc...

Start Testing Button

<input type="button" value="Start Testing" onclick="javascript:TC =
'EmptySearch';doTesting(false);" />

Load event for iframe (to continue testing after postback)

<div style="text-align: center">
            <iframe src="../Forms/SearchUser.aspx" id="ifrmUserSearch" name="ifrmUserSearch"
                width="96%" height="500px" onload="doTesting(true);" />
        </div>

*NOTE: Both the pages need to be on the same machine to be tested (cross domain testing is not allowed)

mA8a6.gif

Here's a sample image explaining my method:

And here's the wrapper page code:

C#
1.<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2.<html xmlns="http://www.w3.org/1999/xhtml">
3.<head>
4.    <title>Test User Search </title>
5.<script type="text/javascript" src="../Testing.js" ></script>
6.<script type="text/javascript"> 
7.var TC = "";
8.var uName = "admin";    var userRoleDDLIndex1 = 1; 
  var userRoleDDLIndex2 = 2; /* zero-based */
9.var email = "";
10. 
11.var ifrm = document.getElementById("ifrmUserSearch");
12.var doc = getDocument("ifrmUserSearch"); 
13. 
14.var lnkbtnSort1="";         var txtNameFind=""; var ddltxtNameFind="";
15.var lnkbtnSort2="";         var ddlOrgFind="";  var lnkbtnUserRole="";
16.var ddlUserRoleFind ="";    var lnkbtnEmail=""; var txtEmailFind="";
17.var ddltxtEmailFind ="";
18. 
19.var lnkbtnAddManage ="";var btnSearch="";var btnResetSearch="";
20.var ibtnFirst="";var ibtnPrevious="";var ibtnNext="";var ibtnLast="";var ddlPages="";
21. 
22.var chkStop = "";var chkTC01 = "";var chkTC02 = "";
       var chkTC03 = "";var chkTC04 = "";var chkTC05 = "";
23.var txtLog = "";
24. 
25.//HT:***IMP: In pages with AJAX postbacks (i.e. ALL the pages)the iframe's 
   //"onload" is never called.
26.// So need to add a statement: if(window.parent != null) window.parent.doTesting(true);
27.// In the function EndRequestHandler(sender, args) (i.e. Ref Search.aspx, line: 18
28.function doTesting(isResult)
29.{
30.        setVariables(); 
31.        
32.        if(chkStop.checked) return;//STOP if checkbox is checked!
33.        
34.        //IE calls onload 'before' loading and mozilla does it 'after'
35.// http://maniish.wordpress.com/2006/12/22/documentreadystate-alternative-in-mozilla/#comment-1989
36.        if(doc.readyState && doc.readyState!="complete") return;        
37.        var skipNext = false;
38.        
39.        switch (TC)
40.        {        
41.            case "EmptySearch":
42.                if(isResult)
43.                {chkTC01.checked = false;  EmptySearch(true);}
44.                else
45.                {
46.                    if(chkTC01.checked) EmptySearch(false);
47.                    else {skipNext = true; TC = "HeaderSorting1";}
48.                }
49.            break;
50.            case "HeaderSorting1": 
51.                if(isResult)
52.                {HeaderSorting1(true);TC = "HeaderSorting2";}
53.                else
54.                {
55.                    if(chkTC02.checked) HeaderSorting1(false);
56.                    else {skipNext = true;TC = "PaginationNxt";}
57.                }
58.                        break;
59.                        case "HeaderSorting2": 
60.                if(isResult)
61.                {chkTC02.checked = false;  HeaderSorting2(true);}
62.                else
63.                {
64.                    if(chkTC02.checked) HeaderSorting2(false);
65.                    else {skipNext = true;TC = "PaginationNxt";}
66.                }
67.                        break;
68.                        /* Start : PAGINATION Test Cases*/
69.                        /* Special case - need to recurse for other paginations (so set TC)*/
70.                    case "PaginationNxt": 
71.                if(isResult)
72.                {Pagination(true,"Next"); TC = "PaginationPrev";}
73.                else
74.                {
75.                    if(chkTC03.checked) Pagination(false,"Next");
76.                        else {skipNext = true;TC = "FullSearch";}
77.                    }
78.                    break;
79.                    case "PaginationPrev": 
80.                if(isResult)
81.                {Pagination(true,"Prev"); TC = "PaginationLast";}
82.                else
83.                {
84.                    if(chkTC03.checked) Pagination(false,"Prev");
85.                        else {skipNext = true;TC = "FullSearch";}
86.                    }
87.            break;
88.            case "PaginationLast": 
89.                if(isResult)
90.                {Pagination(true,"Last"); TC = "PaginationFirst";}
91.                else
92.                {
93.                    if(chkTC03.checked) Pagination(false,"Last");
94.                        else {skipNext = true;TC = "FullSearch";}
95.                    }
96.            break;
97.            case "PaginationFirst": 
98.                if(isResult)/* Full Search will be continued */
99.                {Pagination(true,"First");TC = "PaginationGoto";}
100.                else
101.                {
102.                    if(chkTC03.checked) Pagination(false,"First");
103.                        else {skipNext = true;TC = "FullSearch";}
104.                    }
105.            break; 
106.            case "PaginationGoto": 
107.                if(isResult)/* Full Search will be continued */
108.                {chkTC03.checked = false; Pagination(true,"Goto");}
109.                else
110.                {
111.                    if(chkTC03.checked) Pagination(false,"Goto");
112.                        else {skipNext = true;TC = "FullSearch";}
113.                    }
114.            break;            
115.            /* End : PAGINATION Test Cases*/
116.                        case "FullSearch": 
117.                if(isResult)
118.                {chkTC04.checked = false;  FullSearch(true);}
119.                else
120.                {
121.                    if(chkTC04.checked) FullSearch(false);
122.                    else {skipNext = true;TC = "RandomSearch";}
123.                }
124.                        break;
125.                        case "RandomSearch": 
126.                if(isResult)
127.                {chkTC05.checked = false;  RandomSearch(true);}
128.                else
129.                {
130.                    if(chkTC05.checked) RandomSearch(false);
131.                    else {skipNext = true;TC = "ResetSearch";}
132.                }
133.                        break;
134.                        case "ResetSearch": 
135.                if(isResult)
136.                {ResetSearch(true); TC = ""; alert("User Search Testing complete");}
137.                else {ResetSearch(false);}
138.                        break;
139.                }
140.        //Testing continues if it was just a result or we need to skip to the next test-case
141.        //HT: MAKE SURE TC is set or it'll be an infinite loop!!!
142.        if(isResult || skipNext && TC != "")doTesting(false);
143.}
144. 
145.function FullSearch(done) 
146.{
147.        if(done)        {alert("Result:TC04: Search for : Name(admin), 
                Role(Admin) & Email(admin@cclear.com)"); return;}
148. 
149.        alert("Search:TC04: Full Search");
            //Need onchange to set value!
150.        txtNameFind.value = uName; txtNameFind.onchange();
            //Need onchange to set value!
151.        ddlUserRoleFind.selectedIndex = userRoleDDLIndex1; ddlUserRoleFind.onchange();
152.        txtEmailFind.value = email; txtEmailFind.onchange();//Need onchange to set value!
153.        btnSearch.click();
154.}
155. 
156.function RandomSearch(done) 
157.{
158.        if(done)        {alert("Result:TC05: Search for : Role(User)"); return;}
159. 
160.        alert("Search:TC05: Random Search");
            //Need onchange to set value!
161.        txtNameFind.value = ""; txtNameFind.onchange();
            //Need onchange to set value!
162.        ddlUserRoleFind.selectedIndex = userRoleDDLIndex2; ddlUserRoleFind.onchange();
163.        txtEmailFind.value = ""; txtEmailFind.onchange();//Need onchange to set value!
164.        btnSearch.click();
165.}
166. 
167.function setVariables()
168.{    
169.    ifrm = document.getElementById("ifrmUserSearch");    
170.    doc = getDocument('ifrmUserSearch');    
171.    if(doc == null)return;
172.    
173.    /* Name column */
174.    lnkbtnSort1 = doc.getElementById("ctl00_cphChild_gvListUser_ctl01_lnkbtnName");
175.    txtNameFind = doc.getElementById("ctl00_cphChild_gvListUser_ctl01_txtNameFind");
176.    ddltxtNameFind = doc.getElementById("ctl00_cphChild_gvListUser_ctl01_ddltxtNameFind");
177.    /* Org column */
178.    lnkbtnSort2 = doc.getElementById("ctl00_cphChild_gvListUser_ctl01_lnkbtnOrg");
179.    ddlOrgFind = doc.getElementById("ctl00_cphChild_ddlOrgFind");
180.    /* UserRole column */
181.    lnkbtnUserRole = doc.getElementById("ctl00_cphChild_gvListUser_ctl01_lnkbtnUserRole");
182.    ddlUserRoleFind = doc.getElementById("ctl00_cphChild_gvListUser_ctl01_ddlUserRoleFind");
183.    /* Email column */
184.    lnkbtnEmail = doc.getElementById("ctl00_cphChild_gvListUser_ctl01_lnkbtnEmail");
185.    txtEmailFind = doc.getElementById("ctl00_cphChild_gvListUser_ctl01_txtEmailFind");
186.    ddltxtEmailFind = doc.getElementById("ctl00_cphChild_gvListUser_ctl01_ddltxtEmailFind");
187.    
188.    //HT: The following IDs work only for Pagesize:4 (total records: 10)
189.    ibtnFirst = doc.getElementById("ctl00_cphChild_gvListUser_ctl05_ibtnFirst");
190.    ibtnPrevious = doc.getElementById("ctl00_cphChild_gvListUser_ctl07_ibtnPrevious");
191.    ibtnNext = doc.getElementById("ctl00_cphChild_gvListUser_ctl07_ibtnNext");
192.    ibtnLast = doc.getElementById("ctl00_cphChild_gvListUser_ctl07_ibtnLast");
193.        
194.    ddlPages = doc.getElementById("ctl00_cphChild_gvListUser_ctl07_ddlPages");
195.    
196.    commonSetVariables();// set common controls
197.}
198.    </script>
199. 
200.</head>
201.<body style="font-family: Verdana; font-size: 12px; margin: 1px 1px 1px 1px;">
202.    <h3>
203.        UserSearch Automated Testing</h3>
204. 
205.        <div style="position:absolute;top:10px;left:500px;">
            <a href="../TestingSiteMap.html">Testing Sitemap</a> </div>
206.    <div style="vertical-align: middle">
207.        [<input type="checkbox" id="chkTC01" name="chkTC01" 
                checked="checked" />: TC01:Empty Search]<br />
208.        [<input type="checkbox" id="chkTC02" name="chkTC02" 
                 checked="checked" />: TC02:Header Sorting]<br />
209.        [<input type="checkbox" id="chkTC03" name="chkTC03" 
                checked="checked" />: TC03:Pagination (Next, Prev, First, 
                Last & Goto Page] Note: IDs tested for Pagesize:4 (total records: 10)<br />
210. 
211.        [<input type="checkbox" id="chkTC04" name="chkTC04" 
                 checked="checked" />: TC04:Full Search]<br />
212.        [<input type="checkbox" id="chkTC05" name="chkTC05" 
                 checked="checked" />: TC05:Random Search]<br />
213.        
214.        <input type="button" value="Start Testing" onclick="javascript:TC = 
                'EmptySearch';doTesting(false);" />
215.        [<input type="checkbox" id="chkStop" name="chkStop" />Stop]
216.        <br />
217.         <br />
218.         <!--<textarea id="txtLog" rows="5" cols="" ></textarea>-->
219. 
220.        <div style="text-align: center">
221.            <iframe src="../../Forms/Search/SearchUser.aspx" id="ifrmUserSearch" 
                name="ifrmUserSearch"
222.                width="96%" height="500px" onload="doTesting(true);" />
223.        </div>
224.    </div>    
225.</body>
226.</html>

Logic and Flow (How Does it Recursively Loop)

As this is "your own code" there will be a one-time setup code which will require some effort. For example,

function setVariables()

You’ll need to declare variables for each control in the child page (page to be tested) used for testing. You’ll need to view-source the rendered page and pickup control Ids from it. So that controls can be directly accessed in the code-logic.

function doTesting(isResult)

This is the main function which is called ‘recursively’ until you mark a flag in your code to notify that the testing is complete (i.e. TC = "";). The isResult argument is meant to separate the server postback call of this function. In this function, after some preliminary checks there’s a Switch-case statement which determines in which test-case does this execution fall.

C#
switch (TC)
        {        
            case "EmptySearch":
                if(isResult)
                {chkTC01.checked = false;  EmptySearch(true);//Tested, show notification}
                else
                {
                    if(chkTC01.checked) EmptySearch(false); // perform test
                   //set test-case variable & skip to next }
                   else {skipNext = true; TC = "HeaderSorting1";
               }
            break;

In each case the flow is as follows:

  • There’s a function for each test-case like function EmptySearch(done)
  • If isResult = true – means the test is already conducted so we uncheck the checkbox and call the test-case function with flag done=true
  • If test-case is yet to be performed then first we check whether the user has checked that test-case or not. Then we call the test-case routine.
  • ATTENTION: what happens at this point is tricky, so pay attention. Now from the test-case routine the script might trigger a postback which will mean that the iframe page is refreshed. The script execution can’t "wait" until the server response arrives so we exit the function after - EmptySearch(false);
  • The TRICK is to trap the execution flow and get it back to the next step. There’re two steps here:
    • Test-case is done so show notification. After this as the flow exits the Switch-case we’ve another recursion to make sure we trigger the next test-case. Here it is:
    • C#
      //Testing continues if it was just a result or we need to skip to the next test-case
      //HT: MAKE SURE TC is set or it'll be an infinite loop!!!
      if(isResult || skipNext && TC != "")doTesting(false);
  • This will take the flow back to the same test-case in the Switch-case and this time execute the following code:
  • C#
    else {skipNext = true; TC = "HeaderSorting1"; //set test-case variable  & skip to next
  • And finally, we get back to the recursion, this time with skipNext = true; and a new test-case set TC = "HeaderSorting1";
  • So, this recursion takes the flow back to the Switch-case but this time it falls into the next case and this cycle is repeated until all the test-cases are done.
  • Sorry for the complex logic but this is really the only way I could find to continue the flow and also keep the notifications in between. It’s tricky but once you understand the mechanism you’ll know that its mandatory. Of course I’m all ears for those logic-gurus who can optimize my code in any form.
  • For now, I’ve tried to bundle some common functions in a Testing.js file so don’t panic if you don’t see all the functions in the same file

I hope you can spare me from explaining how to set control values and then trigger the button click events. I’ve implemented/tested this for AJAX based web-app so this will definitely work in a simple manner for those plain post-back web-apps.

ADVANCED: For AJAX based web-app all you need to add is a callback to our doTesting(true);on top of the page as follows:

$(document).ready(function(){if(window.parent
!= self) window.parent.doTesting(true);}

Conclusion

I developed this method because I needed something which was my own and allowed full-freedom to tweak-n-tap. Because now-a-days web app are only getting more and more complex, tools are cool but sometimes they limit the functionality. I believe, apart from the recursive-logic, the rest of the code is simple and it makes it easier to tweak your test-cases, add, edit and remove test-cases as your web-app evolves. I hope its helpful.

Please share your thoughts. And if anyone is interested I've also posted a YouYube video showing it live in action: Simple JS based Auto Testing User Search

I'd like to know your suggestions and idea about the same and what you prefer. I see many usages of this method like - auto-testing, showing an automated demo, automated-data-entry... but everything on the same domain.

License

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


Written By
India India
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --