Friday, October 7, 2011

Selenium Tests:: How to Make them better? Step1: Make them more Readable

I like COBOL from old days, because its very easy to read through, if written well. Its very much like reading a news paper. People love to have more sensible, readable code, isn't it. It should definitely not like some 8085/86 programming? :-(

So anyway Selenium can be written in quite a few programming languages, anyway I picked Java to write this. I am an expert programmer but still I like the way it can be formatted or written so everyone can understand. And I will try to explain this in this post, how to make a Selenium test more readable.


Why do we need to be much constructive and follow certain processes to make it much better?
Well, quite a familiar situation to some of the teams who are practising Agile newly, where a project is very agile, architecture is evolving, requirements are quite evolving, very unclear process, Agile is not followed by books etc there could be so many reasons for this to happen.

In this situation, development or business is still expecting the QA to automate all tests and integrate them in to a build process as a part of Continuous Integration environment etc. QA complains that the build is not stable and without the build being in a good stable condition there's no point in automating tests. WOW.. sounds familiar isn't it?

What're the basic things one should know about UI mapping technique?
Well, as you can imagine UI mapping technique is nothing but an OR, if you are using some traditional automation tools. Where you can store your objects and constantly refer them in your tests.

I would say, its not quite friendly. You have to do a bit of work to actually find properties you want selenium to use to recognize the objects, by using some open source tools/ add-ons (like, Firebug) or looking at the code or whatever.

so, how to Automate and still make the Maintainability a less effort?

1st Step, I would to make it more readable is by creating a very basic UI map. UI mapping is technique used to make the Web elements (Objects for QTP folks) properties all stored in a single place. It has the following advantages:

1.  Any property change for a given web element (ID, Name, Link, CSS, XPATH etc) in the future is quite easy to make. Because there's only 1 place to go to make such a change, your UI map. 

For traditional automation tool users, its like an Object repository (OR), where you will update or add Mandatory/ Assisted properties etc

2. Because we're using a single place to store and retrieve all object properties from, you will make a change to an Web element properties once for all.

3. You don't need to be an expert in some special technology or something. You can do it in your comfortable language. It's just a technique and there's no strict rules on how to create it, there's a very good guidance in Selenium documentation section

Okay, enough details. How do we actually do it?
Well, again it depends on few things like the size of the application, architecture of the application, number of modules, size of the each module and how much automation effort is planned for testing this application etc. So far I could see Selenium is used either for smoke testing or for some functional testing, where as it can do lot more.

Anyway, I used to java and I have used a Java file to store my web element mappings and I refer to these mapped elements throughout my test suite..

Well, a Java file where I stored my mappings will look something like below
public class gmailMap {
public String emailID = "id=Email";
public String passWord = "id=Passwd";
public String signInBtn = "id=signIn";
public String waitTime = "30000";
public String InboxCnt = "css=div.XQRo3.UKr6le";
public String emailTable = "//*[@id=\":pd\"]";
public String firstemailFrom = "id=:p9";
public String firstemailSubj = "id=:p6";
public String accName = "id=gbi4t";
public String signOut = "//*[@id=\"gb_71\"]";
}

and tests might look something like below when they use the UI mapping technique followed.
@Test
public void testGmailLogIn() throws Exception {
gmailMap UImap = new gmailMap();
selenium.open("/");
selenium.selectWindow(null);
selenium.type(UImap.emailID, "<<username>>");
selenium.type(UImap.passWord, "<<password>>");
selenium.click(UImap.signInBtn);
selenium.waitForPageToLoad(UImap.waitTime);
String newEmails = selenium.getText(UImap.InboxCnt);
System.out.println(newEmails);
verifyTrue(selenium.isElementPresent(UImap.emailTable));
String firstEmail = selenium.getText(UImap.firstemailFrom);
String fullSubjectText = selenium.getText(UImap.firstemailSubj);
String accName = selenium.getText(UImap.accName);
System.out.println(UImap.firstemailFrom);
System.out.println(UImap.firstemailSubj);
System.out.println(UImap.accName);
selenium.click(UImap.accName);
assertTrue(selenium.isElementPresent(UImap.signOut));
selenium.click(UImap.accName);
selenium.click(UImap.signOut);
selenium.waitForPageToLoad(UImap.waitTime);
selenium.selectWindow("null");
//selenium.click("id=PersistentCookie");
}

Here as you can see, there's only element names are used, suggesting the reader that I want to use "selenium" to "type", some text "username" in the control called "UImap.emailID".

Please note, the webElement name given in the UImap should be very understandable. Whatever it is, but naming convention is important too so the user can easily understand it by looking at.

There's a few different ways to do it, one of the other popular way of doing it, especially for people from Java world, is using the properties file. In this approach, you will use a simple .properties file where a key=value pairs of representation is used. So a simple gmailUImap.properties might look something like below
UI_Email="id=Email"
UI_passWord = "id=Passwd";

and your test might look something like below.. I am just writing it here, I didn't ran this myself, so if it doesn't run well, you can correct it or pass on your comments on this post as well.

public class gmailClass extends SeleneseTestCase{
public String PROP_FILE="gmailUI.properties";
public String UIemail;
public String UIpassWord;

@BeforeClass
public void setUp() throws Exception {
SeleniumServer selServ = new SeleniumServer();
selServ.boot();
setUp("https://accounts.google.com/", "*firefox");
selenium.windowMaximize();
selenium.windowFocus();
}


public gmailClass() throws Exception{
//create and load properties
    try{  
        InputStream is = gmailClass.class.getResourceAsStream(PROP_FILE);  
        Properties prop = new Properties();  
        prop.load(is);  
        UIemail = prop.getProperty("UI_Email");
        UIpassWord = prop.getProperty("UI_passWord");
        is.close();  
        System.out.println(emailID);
        /* code to use values read from the file*/  
      }catch(Exception e){  
        System.out.println("Failed to read from " + PROP_FILE + " file.");  
      }
}

@Test
public void testGmailLogIn() throws Exception {
gmailMap UImap = new gmailMap();
selenium.open("/");
selenium.selectWindow(null);
selenium.type(UIemail, "<<username>>");
selenium.type(UIpassWord, "<<password>>");
.
..
...
....
}


Whatever way you chose to write your tests, this process makes it a bit better readable test, also making it more maintainable. But, this is missing something, we're giving the precise data to run the test. So its obviously not so complete to data drive it, especially when we want to test it against positive, negative, boundary conditions and most importantly if you want to load test it.. this is our next step

If you have any comments or suggestions, feel free to pass them on!

Selenium Tests: How to make them Better

May be you should be thinking about writing selenium tests in a better manner considering various things about the application, areas of testing that are in-scope for automation, time constraints you are running under etc etc. However below are some examples of tests that can be written in different ways to make it more maintainable, readable, scalable..

If you're using a sample application to record a simple Selenium test. Of course, you can use selenium IDE to record your first test and then export the same to your development IDE (like Eclipse, Visual Studio etc) and try to run it. When you have actually exported your recorded test, it might look something like below (Note: I have used Java code throughout this)

package com.example.tests;
import com.thoughtworks.selenium.*;
import java.util.regex.Pattern;


public class Untitled extends SeleneseTestCase {
public void setUp() throws Exception {
setUp("http://www.mortgagecalculator.org/", "*chrome");
}
public void testUntitled() throws Exception {
selenium.open("/");
selenium.type("name=param[homevalue]", "150000");
selenium.select("name=param[credit]", "label=Excellent");
selenium.type("name=param[principal]", "125000");
selenium.select("name=param[rp]", "label=New Purchase");
selenium.type("name=param[interest_rate]", "2.5");
selenium.type("name=param[term]", "25");
selenium.select("name=param[start_month]", "label=Jan");
selenium.select("name=param[start_year]", "label=2010");
selenium.type("name=param[property_tax]", "1.1");
selenium.type("name=param[pmi]", "0.35");
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad("30000");
String Monthly_Pay = selenium.getText("css=td > h3");
}
}

Manual test for the above class is simple;
1. Open MortgageCalculator.org web page
2. Enter homevalue as 150,000
3. Enter Credit Profile as Excellent
4. Enter Loan Amount as 125,000
5. Enter Loan Purpose
6. Enter Loan Term
7. Enter Start Date
8. Enter Property Tax %
9. Enter PMI %
10. Click Calculate button
11. Finally, verify the Monthly Payment

Now the first thing that one would want to do is to data drive it. May be from an excel file or from a database or whatever.. Here we will try to do the same using Excel which is most widely used for data driven frameworks. If you're using TestNG (as I do) you can actually look at one good article about using TestNG capabilities to data drive a selenium test at Mahesh's blog. I am also using the same data driver here, anyway as a programmer or a tester with common understanding of programming as you could imagine the basic code will change something like below...

public void testMortgages(String homeValue, String creditRate, String principalAmt,String typePurchase, String intRate, String termYrs, String startMonth, String startYr, String propTax, String pmiPercent, String expMthlyPay, String actMthlyPay) throws Exception {
selenium.type("name=param[homevalue]", homeValue);
selenium.select("name=param[credit]", "label=" +creditRate);
selenium.type("name=param[principal]", principalAmt);
selenium.select("name=param[rp]", "label="+typePurchase);
selenium.type("name=param[interest_rate]", intRate);
selenium.type("name=param[term]", termYrs);
selenium.select("name=param[start_month]", "label=" +startMonth);
selenium.select("name=param[start_year]", "label=" +startYr);
selenium.type("name=param[property_tax]", propTax);
selenium.type("name=param[pmi]", pmiPercent);
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad("30000");
String monthlyPay = selenium.getText("css=td > h3");
Assert.assertEquals(monthlyPay, expMthlyPay);
System.out.println("Actual Monthly Pay is: " + monthlyPay);
}

Basically we have replaced all the static data with variables. How these variables can be picked up from an excel file? well, we use jxl to do the job of retrieving and/or reading excel file etc and TestNG to do the rest of the job, to use the data returned by a method that uses jxl capabilities to read an excel file and then run a test...

Here as an example, I used Mahesh's code for reading an excel file using jxl

public String[][] getTableArray(String xlFilePath, String sheetName, String tableName) throws Exception{
        String[][] tabArray=null;
       
            Workbook workbook = Workbook.getWorkbook(new File(xlFilePath));
            Sheet sheet = workbook.getSheet(sheetName);
            int startRow,startCol, endRow, endCol,ci,cj;
            Cell tableStart=sheet.findCell(tableName);
            startRow=tableStart.getRow();
            startCol=tableStart.getColumn();


            Cell tableEnd= sheet.findCell(tableName, startCol+1,startRow+1, 100, 64000,  false);              


            endRow=tableEnd.getRow();
            endCol=tableEnd.getColumn();
            System.out.println("startRow="+startRow+", endRow="+endRow+", " +
                    "startCol="+startCol+", endCol="+endCol);
            tabArray=new String[endRow-startRow-1][endCol-startCol-1];
            ci=0;


            for (int i=startRow+1;i<endRow;i++,ci++){
                cj=0;
                for (int j=startCol+1;j<endCol;j++,cj++){
                    tabArray[ci][cj]=sheet.getCell(j,i).getContents();
                }
            }
        return(tabArray);
    }

and created a @dataprovider annotation to read from a given file using the above method..


    @DataProvider(name = "DP2")
    public Object[][] createData2() throws Exception{
        Object[][] retObjArr=getTableArray("lib\\data1.xls","Sheet1","mrtgCalcData1");
        return(retObjArr);
    }

But for my Test method to use this data provider, I have to clearly specify the same who's the data provider for that test, so I will add  @Test (dataProvider = <<name>>) annotation for my test, that would make it look like below...

@Test (dataProvider = "DP2")
<<<<your selenium test method>>>

And that's it. You just need to add a Setup(), Teardown() methods, if you're using Junit framework or some test annotations if you're using TestNG framework to start the selenium server and stop it when the job done..!

package com.tests;


import com.thoughtworks.selenium.*;
//import com.tests.publicLibrary.*;
import org.junit.AfterClass;
import org.openqa.selenium.server.SeleniumServer;
import org.testng.Assert;
import org.testng.annotations.*;
import java.io.File;
import jxl.*;


@SuppressWarnings("deprecation")
public class mortgageCalc extends SeleneseTestCase{
   
    @BeforeClass
    public void beforeJobStarted() throws Exception {
        SeleniumServer seleniumserver=new SeleniumServer();
        seleniumserver.boot();
        seleniumserver.start();
        setUp("http://www.mortgagecalculator.org/", "*firefox");
        selenium.open("/");
        selenium.windowMaximize();
        selenium.windowFocus();
    }


    @DataProvider(name = "DP2")
    public Object[][] createData2() throws Exception{
        Object[][] retObjArr=getTableArray("lib\\data1.xls","Sheet1","mrtgCalcData1");
        return(retObjArr);
    }  
   
@Test (dataProvider = "DP2")
public void testMortgages(String homeValue, String creditRate, String principalAmt,String typePurchase, String intRate, String termYrs, String startMonth, String startYr, String propTax, String pmiPercent, String expMthlyPay, String actMthlyPay) throws Exception {
selenium.type("name=param[homevalue]", homeValue);
selenium.select("name=param[credit]", "label=" +creditRate);
selenium.type("name=param[principal]", principalAmt);
selenium.select("name=param[rp]", "label="+typePurchase);
selenium.type("name=param[interest_rate]", intRate);
selenium.type("name=param[term]", termYrs);
selenium.select("name=param[start_month]", "label=" +startMonth);
selenium.select("name=param[start_year]", "label=" +startYr);
selenium.type("name=param[property_tax]", propTax);
selenium.type("name=param[pmi]", pmiPercent);
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad("30000");
String monthlyPay = selenium.getText("css=td > h3");
Assert.assertEquals(monthlyPay, expMthlyPay);
System.out.println("Actual Monthly Pay is: " + monthlyPay);
}  
   
   
    @AfterClass
    public void afterJobDone(){
        selenium.close();
        selenium.stop();
    }
   
    public String[][] getTableArray(String xlFilePath, String sheetName, String tableName) throws Exception{
        String[][] tabArray=null;
       
            Workbook workbook = Workbook.getWorkbook(new File(xlFilePath));
            Sheet sheet = workbook.getSheet(sheetName);
            int startRow,startCol, endRow, endCol,ci,cj;
            Cell tableStart=sheet.findCell(tableName);
            startRow=tableStart.getRow();
            startCol=tableStart.getColumn();


            Cell tableEnd= sheet.findCell(tableName, startCol+1,startRow+1, 100, 64000,  false);              


            endRow=tableEnd.getRow();
            endCol=tableEnd.getColumn();
            System.out.println("startRow="+startRow+", endRow="+endRow+", " +
                    "startCol="+startCol+", endCol="+endCol);
            tabArray=new String[endRow-startRow-1][endCol-startCol-1];
            ci=0;


            for (int i=startRow+1;i<endRow;i++,ci++){
                cj=0;
                for (int j=startCol+1;j<endCol;j++,cj++){
                    tabArray[ci][cj]=sheet.getCell(j,i).getContents();
                }
            }
        return(tabArray);
    }
}

If you run this using TestNG, it should work fine. You can use any other framework by making lil updates to the above class, that is out of scope for this.. But what can we do better, here? Is this the best automated test case, I can ever have using Selenium. Obviously not, because one could use his/ her own intelligence to come up with a brilliant solution to make it more maintainable, scalable etc. However there's pretty basic things that one should consider that're already listed in selenium's website. Below are some examples of how this test can be made better readable, maintainable, scalable. I will try to explain them in my next few posts..