Saturday, November 13, 2010

Testing sorting feature of an application

All applications display data and allow the user to sort the data on some column. I am sure you would have written or executed test cases to test the sorting feature of your application. There are many strategies to automate such test. In this post I will try to explain a strategy I use widely.

Data is generally displayed in the form of a table (webtable) with each row containing one record. Generally in web applications the sorting feature is given on some or all column of the table. Let’s look at one such website that displays data in this fashion. Navigate to www.vcdq.com


This website as you can see has information on the latest movie releases. It allows you to sort the data on two of the columns, Date and Release. You can sort the data by clicking the respective column header. Both columns can be sorted in ascending and descending order. A small arrow icon is indicating the current sorting order next to the column header text.

My strategy involves the following
a)    Reading all the data of this table into a java ArrayList. Lets call this ExpectedList
b)    Perform the sort on the application by clicking the column header
c)    Read the data from the table again and store it in another ArrayList. Lets call this ActualListAfterSort
d)    Sort the ExpectedList
e)    Compare ExpectedList with ActualListAfterSort and verify that the order of the elements is same.

There is a distinction between the ArrayList that holds primitive datatypes like String or int and the one I use. I use a user defined class to hold the rows of data. Each member element of this class represents one column of the table. The class is defined below.

public class ReleaseInfoTableRecord {
String standard;
Date date;
String format;
String source;
String release;
double imdb;
int disks;
String group;
int ratingAudio;
int ratingVideo;

public ReleaseInfoTableRecord(String standard, Date date, String format,
        String source, String release, double imdb, int disks, String group,
        int ratingAudio, int ratingVideo) {
    super();
    this.standard = standard;
    this.date = date;
    this.format = format;
    this.source = source;
    this.release = release;
    this.imdb = imdb;
    this.disks = disks;
    this.group = group;
    this.ratingAudio = ratingAudio;
    this.ratingVideo = ratingVideo;
}
}

I will insert objects of this class into the ArrayList. An object will represent one row of the table. This is how will insert record within the ArrayList.

ArrayList<ReleaseInfoTableRecord> LatestMovieReleaseDisplayed=new ArrayList<ReleaseInfoTableRecord>();
        
LatestMovieReleaseDisplayed.add(new ReleaseInfoTableRecord(standard, date, format, 
                    source, release, imdb, disks, group, ratingAudio, ratingVideo  ) );

Now in order to sort the data of the ArrayList I would have to define some comparators. I will have to define comparators for all the sort features that I wish to test. For example If the application under test allows sorting the data on ascending and descending order on column Date and I want to test this feature than I need to define two comparators one for ascending and other for descending on date. I will be defining these comparators in the class ReleaseInfoTableRecord.

static final Comparator<ReleaseInfoTableRecord> SORT_DATE_ASCENDING =  new Comparator<ReleaseInfoTableRecord>(){
    public int compare(ReleaseInfoTableRecord o1, ReleaseInfoTableRecord o2) {
        int result = o1.getDate().compareTo(o2.getDate());
        if (o1.getDate().equals(o2.getDate()))
            result =o1.getRelease().compareTo(o2.getRelease())*-1;
        return (result);
    }
};

static final Comparator<ReleaseInfoTableRecord> SORT_DATE_DESCENDING =  new Comparator<ReleaseInfoTableRecord>(){
    public int compare(ReleaseInfoTableRecord o1, ReleaseInfoTableRecord o2) {
        int result = o1.getDate().compareTo(o2.getDate())*-1;
        if (o1.getDate().equals(o2.getDate()))
            result =o1.getRelease().compareTo(o2.getRelease());
        return (result);
    }
};


The hashCode and equals method of the data class must be overridden to make the comparison possible. This overriden methods can be genarated in eclipse by doing right click source-->generate hashcode and equals. You will se this below in the full class code.

If the two records have the same date then the application sorts on the basis of Release name. Note the logic for each comparator when the date is same. This is specific to this application and the application that you are testing may have a different logic. Go thru the application logic carefully before defining the comparators.

Now we define the actual test method that tests the sorting feature. Note that I have read the data into the ArrayList dataAsDisplayedInDefaultView in an earlier step. The code in the while loop is instruction that performs the sorting on the web application i.e clicks the column header link repeatedly till the appropriate arrow icon appears. Your application may involve steps different than this to invoke sort.

@Test
    public void testSortingDateAscending() {            
        int i=0;
        //Click Date column header till the small up arrow is visible next to <Date> column header, 
        //don't get confused by the title of the up arrow image        
        while (false==selenium.isElementPresent("//table//th[2]/a/img[@title='sort descending']")&&i<2){
        selenium.click("link=Date");
        selenium.waitForPageToLoad("30000");
        }        
        
        ArrayList<ReleaseInfoTableRecord> dataAsDisplayedAfterSort=getLatestMovieReleaseDisplayed();
        //make a copy of the arraylist that holds the displayed data 
        ArrayList<ReleaseInfoTableRecord> expectedOrder=dataAsDisplayedInDefaultView;
        //sort the copy on Date Descending, this is how the data is sorted in the default view on this website
        Collections.sort(expectedOrder, ReleaseInfoTableRecord.SORT_DATE_ASCENDING);
        //This will pass only if the elements in both the lists appear in the same order
        org.testng.Assert.assertTrue(dataAsDisplayedAfterSort.equals(expectedOrder));        
    }

Below you can see both the classes in their full glory. ReleaseInfoTableRecord class that will be used to hold the data (record of the table) and SortingTest is the test class.

package test;


import java.util.Comparator;
import java.util.Date;

public class ReleaseInfoTableRecord {
String standard;
Date date;
String format;
String source;
String release;
double imdb;
int disks;
String group;
int ratingAudio;
int ratingVideo;

public ReleaseInfoTableRecord(String standard, Date date, String format,
        String source, String release, double imdb, int disks, String group,
        int ratingAudio, int ratingVideo) {
    super();
    this.standard = standard;
    this.date = date;
    this.format = format;
    this.source = source;
    this.release = release;
    this.imdb = imdb;
    this.disks = disks;
    this.group = group;
    this.ratingAudio = ratingAudio;
    this.ratingVideo = ratingVideo;
}



@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((date == null) ? 0 : date.hashCode());
    result = prime * result + disks;
    result = prime * result + ((format == null) ? 0 : format.hashCode());
    result = prime * result + ((group == null) ? 0 : group.hashCode());
    long temp;
    temp = Double.doubleToLongBits(imdb);
    result = prime * result + (int) (temp ^ (temp >>> 32));
    result = prime * result + ratingAudio;
    result = prime * result + ratingVideo;
    result = prime * result + ((release == null) ? 0 : release.hashCode());
    result = prime * result + ((source == null) ? 0 : source.hashCode());
    result = prime * result + ((standard == null) ? 0 : standard.hashCode());
    return result;
}
@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    ReleaseInfoTableRecord other = (ReleaseInfoTableRecord) obj;
    if (date == null) {
        if (other.date != null)
            return false;
    } else if (!date.equals(other.date))
        return false;
    if (disks != other.disks)
        return false;
    if (format == null) {
        if (other.format != null)
            return false;
    } else if (!format.equals(other.format))
        return false;
    if (group == null) {
        if (other.group != null)
            return false;
    } else if (!group.equals(other.group))
        return false;
    if (Double.doubleToLongBits(imdb) != Double.doubleToLongBits(other.imdb))
        return false;
    if (ratingAudio != other.ratingAudio)
        return false;
    if (ratingVideo != other.ratingVideo)
        return false;
    if (release == null) {
        if (other.release != null)
            return false;
    } else if (!release.equals(other.release))
        return false;
    if (source == null) {
        if (other.source != null)
            return false;
    } else if (!source.equals(other.source))
        return false;
    if (standard == null) {
        if (other.standard != null)
            return false;
    } else if (!standard.equals(other.standard))
        return false;
    return true;
}
public String getStandard() {
    return standard;
}
public void setStandard(String standard) {
    this.standard = standard;
}
public Date getDate() {
    return date;
}
public void setDate(Date date) {
    this.date = date;
}
public String getFormat() {
    return format;
}
public void setFormat(String format) {
    this.format = format;
}
public String getSource() {
    return source;
}
public void setSource(String source) {
    this.source = source;
}
public String getRelease() {
    return release;
}
public void setRelease(String release) {
    this.release = release;
}
public double getImdb() {
    return imdb;
}
public void setImdb(double imdb) {
    this.imdb = imdb;
}
public int getDisks() {
    return disks;
}
public void setDisks(int disks) {
    this.disks = disks;
}
public String getGroup() {
    return group;
}
public void setGroup(String group) {
    this.group = group;
}
public int getRatingAudio() {
    return ratingAudio;
}
public void setRatingAudio(int ratingAudio) {
    this.ratingAudio = ratingAudio;
}
public int getRatingVideo() {
    return ratingVideo;
}
public void setRatingVideo(int ratingVideo) {
    this.ratingVideo = ratingVideo;
}

static final Comparator<ReleaseInfoTableRecord> SORT_DATE_ASCENDING =  new Comparator<ReleaseInfoTableRecord>(){
    public int compare(ReleaseInfoTableRecord o1, ReleaseInfoTableRecord o2) {
        int result = o1.getDate().compareTo(o2.getDate());
        if (o1.getDate().equals(o2.getDate()))
            result =o1.getRelease().compareTo(o2.getRelease())*-1;
        return (result);
    }
};

static final Comparator<ReleaseInfoTableRecord> SORT_DATE_DESCENDING =  new Comparator<ReleaseInfoTableRecord>(){
    public int compare(ReleaseInfoTableRecord o1, ReleaseInfoTableRecord o2) {
        int result = o1.getDate().compareTo(o2.getDate())*-1;
        if (o1.getDate().equals(o2.getDate()))
            result =o1.getRelease().compareTo(o2.getRelease());
        return (result);
    }
};


static final Comparator<ReleaseInfoTableRecord> SORT_RELEASE_ASCENDING =  new Comparator<ReleaseInfoTableRecord>(){
    public int compare(ReleaseInfoTableRecord o1, ReleaseInfoTableRecord o2) {
        int result = o1.getRelease().compareTo(o2.getRelease());
        return (result);
    }
};

static final Comparator<ReleaseInfoTableRecord> SORT_RELEASE_DESCENDING =  new Comparator<ReleaseInfoTableRecord>(){
    public int compare(ReleaseInfoTableRecord o1, ReleaseInfoTableRecord o2) {
        int result = o1.getRelease().compareTo(o2.getRelease());
        return (result * -1);
    }
};

}


package test;


import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;

import org.openqa.selenium.server.SeleniumServer;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import com.thoughtworks.selenium.SeleneseTestCase;

public class SortingTest extends SeleneseTestCase{
    
    ArrayList<ReleaseInfoTableRecord> dataAsDisplayedInDefaultView;

     @BeforeClass
        public void setUp() throws Exception {
            SeleniumServer seleniumserver=new SeleniumServer();
            seleniumserver.boot();
            seleniumserver.start();
            setUp("http://www.vcdq.com", "*firefox");
            selenium.open("/");
            selenium.windowMaximize();
            selenium.windowFocus();
            
            //filter the results to reduce the data-set to manageable levels            selenium.select("filterStandard", "label=SCENE");
            selenium.select("filterTerm3", "label=DVDR");
            selenium.select("filterTerm2", "label=DVD");
            selenium.click("//input[@value='Filter']");
            selenium.waitForPageToLoad("20000");
            dataAsDisplayedInDefaultView=getLatestMovieReleaseDisplayed();
        }    
    
    @Test
    public void testDefaultSorting(){                
        //make a copy of the arraylist that holds the displayed data 
        ArrayList<ReleaseInfoTableRecord> expectedOrder=dataAsDisplayedInDefaultView;
        //sort the copy on Date Descending, this is how the data is sorted in the default view on this website
        Collections.sort(expectedOrder, ReleaseInfoTableRecord.SORT_DATE_DESCENDING);
        //This will pass only if the elements in both the lists appear in the same order
        org.testng.Assert.assertTrue(dataAsDisplayedInDefaultView.equals(expectedOrder));        
    }
    
        
    @Test
    public void testSortingDateAscending() {            
        int i=0;
        //Click Date column header till the small up arrow is visible next to <Date> column header, 
        //don't get confused by the title of the up arrow image
        while (false==selenium.isElementPresent("//table//th[2]/a/img[@title='sort descending']")&&i<2){
        selenium.click("link=Date");
        selenium.waitForPageToLoad("30000");
        }        
        
        ArrayList<ReleaseInfoTableRecord> dataAsDisplayedAfterSort=getLatestMovieReleaseDisplayed();
        //make a copy of the arraylist that holds the displayed data 
        ArrayList<ReleaseInfoTableRecord> expectedOrder=dataAsDisplayedInDefaultView;
        //sort the copy on Date Descending, this is how the data is sorted in the default view on this website
        Collections.sort(expectedOrder, ReleaseInfoTableRecord.SORT_DATE_ASCENDING);
        //This will pass only if the elements in both the lists appear in the same order
        org.testng.Assert.assertTrue(dataAsDisplayedAfterSort.equals(expectedOrder));        
    }
    
    @Test
    public void testSortingReleaseNameAscending(){
        int i=0;
        //Click Date column header till the small up arrow is visible next to <Release> column header, 
        //don't get confused by the title of the up arrow image
        while (false==selenium.isElementPresent("//table//th[6]/a/img[@title='sort descending']")&&i<2){
        //Click Date column header once so that the sort ascending image is visible
        selenium.click("link=Release");
        selenium.waitForPageToLoad("30000");
        i=i+1;
        }
        
        ArrayList<ReleaseInfoTableRecord> dataAsDisplayedAfterSort=getLatestMovieReleaseDisplayed();
        //make a copy of the arraylist that holds the displayed data 
        ArrayList<ReleaseInfoTableRecord> expectedOrder=dataAsDisplayedInDefaultView;
        //sort the copy on Date Descending, this is how the data is sorted in the default view on this website
        Collections.sort(expectedOrder, ReleaseInfoTableRecord.SORT_RELEASE_ASCENDING);
        //This will pass only if the elements in both the lists appear in the same order
        org.testng.Assert.assertTrue(dataAsDisplayedAfterSort.equals(expectedOrder));        
    }
    
    @Test
    public void testSortingReleaseNameDescending(){            
        int i=0;
        //Click Date column header till the small up arrow is visible next to <Release> column header, 
        //don't get confused by the title of the up arrow image        
        while (false==selenium.isElementPresent("//table//th[6]/a/img[@title='sort ascending']")&&i<2) {
        //Click Date column header once so that the sort ascending image is visible
        selenium.click("link=Release");
        selenium.waitForPageToLoad("30000");        
        }
        
        ArrayList<ReleaseInfoTableRecord> dataAsDisplayedAfterSort=getLatestMovieReleaseDisplayed();
        //make a copy of the arraylist that holds the displayed data 
        ArrayList<ReleaseInfoTableRecord> expectedOrder=dataAsDisplayedInDefaultView;
        //sort the copy on Date Descending, this is how the data is sorted in the default view on this website
        Collections.sort(expectedOrder, ReleaseInfoTableRecord.SORT_RELEASE_DESCENDING);
        //This will pass only if the elements in both the lists appear in the same order
        org.testng.Assert.assertTrue(dataAsDisplayedAfterSort.equals(expectedOrder));        
    }
    
    public ArrayList<ReleaseInfoTableRecord> getLatestMovieReleaseDisplayed() {
        ArrayList<ReleaseInfoTableRecord> LatestMovieReleaseDisplayed=new ArrayList<ReleaseInfoTableRecord>();
        
        String standard;
        Date date=null;
        String format;
        String source;
        String release;
        double imdb;
        int disks;
        String group;
        int ratingAudio;
        int ratingVideo;
        
        String rating;
        String imdbCellContent;
        int tableRowCount=selenium.getXpathCount("/descendant::table//tr").intValue();
        
        //read the data from the webtable
        for (int i=1;i<tableRowCount;i++){
            standard=selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[1]");
            DateFormat df = new SimpleDateFormat("MM/dd/yyyy");
            try{
            date=df.parse(selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[2]"));
            }catch (ParseException p){
                System.out.println(p.getMessage());
            }
            
            format=selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[4]");
            source=selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[5]");
            release=selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[6]");
            imdbCellContent=selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[10]");            
            imdb=!(imdbCellContent.equalsIgnoreCase("N/A"))?Double.parseDouble(selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[10]")):0.0;
            disks=Integer.parseInt(selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[11]"));
            group=selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[12]");
            rating=selenium.getText("XPATH=/descendant::table//tr["+i+"]//td[13]");
            ratingVideo=Integer.parseInt(rating.split(" ")[0].split(":")[1]);
            ratingAudio=Integer.parseInt(rating.split(" ")[1].split(":")[1]);
            
            //insert the data as a instances of the class ReleaseInfoTableRecord into the arraylist
            LatestMovieReleaseDisplayed.add(new ReleaseInfoTableRecord(standard, date, format, 
                    source, release, imdb, disks, group, ratingAudio, ratingVideo  ) );
    }
return LatestMovieReleaseDisplayed;
}
    
}

Copy paste these classes into your project and run as testng test to see them in action.

This is the most robust and foolproof strategy I have come across for testing sorting functionality. I use it to test search results, reports etc. You might also want to create test data before invoking the sort i.e create test data items abc, efg, zab or you could just use the existing data in the system.

Sunday, September 5, 2010

Locator for cell containing nbsp in Selenium RC

If you want to point to a table cell containing a &nbsp; (non breaking space) character then you must use the unicode equivalent of nbsp i.e \u00a0 in your locator. Interestingly this is required only in Selenium RC. A simple space would suffice if you were to achieve this in Selenium IDE. Below is the sample HTML code that has the table with the cell containing the nbsp and the selenium RC instruction to highlight that cell.

<table border="1">
<tr>
<td>abcd&nbsp;efgh</td>
<td>abcdefgh</td>
</tr></table>



selenium.highlight("//td[text()='abcd\u00a0efgh']");

Wednesday, July 14, 2010

Introducing SeleniumForum.com, the forum for selenium user

Hi,
I had always felt a lack of a proper forum for selenium users. And ever since clerspace.openqa was discontinued it was ever more important to start one. Google groups just does not have the features of a forum. So here it goes http://www.seleniumforum.com

Please register and start participating.