Wednesday, April 02, 2014

TestNG data provider for Selenium tests

One of the greatest features of the TestNG framework is a DataProvider (http://testng.org/doc/documentation-main.html#parameters-dataproviders). It is worth to use the DataProvider to parameter Selenium based tests to get better flexibility.
Here I am sharing my experience with the TestNG DataProvider.

The first task was to decide how to structure and store test data - an Excel document is a good data storage.
A java class with a test method that required external data should have a dedicated Excel file. The Excel file should have the same name as the class name including package.
The test method should have a dedicated tab in the Excel file. Tab’s name should be identical to method’s name.
For example there is a class com.qamation.tests.ApplicationFormTests with a test a method testFirstName. Then the data file should have name com.qamation.tests.ApplicationFormTests.xls (or xlsx). This test should have data in a tab named testFirstName. 

This approach allows to separate test data for different test environments using different folders. A mapping between environment and a folder can be using a property file. For example, two files with the same name com.qamation.tests.ApplicationFormTests.xls can be saved in different folders “sandbox” and “production”.

In order to get data from an Excel document into a test, I am using Apache POI - the Java API for Microsoft Documents project. The library and documentation can be found at http://poi.apache.org/.

In most cases I am using two data providers/formats.
The first data provider treats each line in an excel spread sheet as a test case data. Columns filled with data corresponds to parameters in a test method. For example, the testMethod2 accepts four parameters:
@Test(dataProvider = "data" dataProviderClass = com.qamation.data.provider.TestDataProvider.class)
public void testMethod2(String value1, String value2, String value3,
                    String value4) {…}

It means that corresponding excel sheet should have data in first 4 columns. The data file should have tab named testMethod2. First line of the sheet is ignored – test data starts from line 2.









In the code, this data provider is annotated as @DataProvider(name=”data”). In order to use this data provider annotate your test method with the following line:
@Test(dataProvider = "data", dataProviderClass = com.qamation.data.provider.TestDataProvider.class)

The second data provider is more complicated. It is based on an idea that for a single input there are several verification points.
For example, when an application form is submitted, I would like to verify that the result page has several expected components.

In addition, similar set of input data may result different outcomes. For example, submitting the same application form as above but with invalid data should result different outcomes.
I want to utilize the same test method, the same Excel document and sheet for more tests.

In order to implement that, I have introduced a “Test Data” block on an excel sheet.
A data block is a set of one or several lines with data. The left part of the block is input and the right part of the block contains expected results. Number of columns in the input and expected sections are defined in a data sheet header. The input data should not duplicated in a single data block.

The first three lines of the data sheet form a data sheet header.
Cell a1 may contain a description of value in a2, for example: number of input fields.
Cell b1 may contain a description of value in b2, for example:  number of fields for expected results.
Finally, Cell c1 may contain a description of value in c2, for example: total test data lines.

The following picture shows three data blocks for a login test. Each data block provides login form input data. The “expected” results contains information that may be used by WebDriver or WebElement findElement(By arg0) to verify a text on the result page.









First three lines of the data sheet form the header. A data block starts with a line where input columns are not empty.
Any line of data starting with  “!--" will be ignored. The string “!--"  in the beginning of a data block will result ignoring the whole data block.

The picture above shows three data blocks.
The first block starts from line 4 with input fields username and password. Line 6 of this data block will be ignored.
The second block starting at line 8 will be ignored.
The fird data block starts from line 10.

In order to use the second data format, add the following annotation to your test method:
@Test(dataProvider = "inputAndOutput", dataProviderClass = com.qamation.data.provider.TestDataProvider.class)

Now let’s look at code that allows to use those data providers.
A data provider method for first format is:
@DataProvider(name = "data")
public static String[][] getData(Method testMethod) throws IOException {
       int numberOfParameters = testMethod.getParameterTypes().length;
       return getDataForClassName(testMethod.getDeclaringClass().getName(),
       testMethod.getName(), numberOfParameters);
}

getDataForClassName goes through several steps including find the excel file, reads it and fills a String[][] to return as the desired data.
Here are two methods that go through excel cells:
private static String[][] fillData(Sheet sheet, int startRow, int rows,
       int startColumn, int endColumn) {
       if (startRow < 1)
              throw new RuntimeException("startRow cannot be less than 1");
       if (startColumn < 1)
              throw new RuntimeException("startColumn cannot be less than 1");
       if (endColumn < startColumn)
              throw new RuntimeException(
                     "endColumn number cannot be less than startColumn number");
       int numberOfParameters = endColumn - startColumn + 1;
       String[][] returnValue = new String[rows][numberOfParameters];
       int ii = 0;
       for (int i = startRow - 1; i < startRow + rows - 1; i++) {
              Row row = sheet.getRow(i);
              if (row == null) {
                     return returnValue;
              }
              int jj = 0;
              for (int j = startColumn - 1; j < endColumn; j++) {                       
                     Cell cell = row.getCell(j);
                     returnValue[ii][jj] = getCellValue(cell);
                     jj++;
              }
              ii++;
       }
       return returnValue;
}

private static String getCellValue(final Cell cell) {
       if (cell == null) return "";
       int cellType = cell.getCellType();
       switch (cellType) {
       case Cell.CELL_TYPE_NUMERIC:
              if (DateUtil.isCellDateFormatted(cell)) {
                     return String.valueOf(cell.getDateCellValue());
              }
else {
                     return String.valueOf(cell.getNumericCellValue());
              }
       case Cell.CELL_TYPE_STRING:
              return cell.getStringCellValue().trim();
              case Cell.CELL_TYPE_BOOLEAN:
                     return Boolean.toString(cell.getBooleanCellValue());
              default:
                     return "";
       }
}


The following is a data provider for to support the second data format:

@DataProvider(name = "inputAndOutput")
public static DataBlock[][] getTestInformation(Method testMethod)
       throws IOException {
       DataBlock[][] testData = getTestData(testMethod);
       return testData;
}

The actual formation of data blocks occurs in the following method:

private static DataBlock[][] getTestData(Method testMethod)
                     throws IOException {
       Sheet sheet = getDataSheet(testMethod);
       Row testProps = sheet.getRow(1);
       String str = getCellValue(testProps.getCell(0));
       int inputColumns = (int) Float.parseFloat(str);
       str = getCellValue(testProps.getCell(1));
       int expectationColumns = (int) Float.parseFloat(str);
       str = getCellValue(testProps.getCell(2));
       int totalDataRows = (int) Float.parseFloat(str);
       int totalDataColumns = inputColumns + expectationColumns;
       String[][] data = fillData(sheet, 4, totalDataRows, 1, totalDataColumns);
             
       ArrayList<DataBlock> dataList = new ArrayList<DataBlock>();
       DataBlock td = null;
       int j = -1;
       boolean dataBlockIsActive = true;
       for (int i = 0; i < data.length; i++) {               
              String[] dataLine = data[i];
              if (dataLine[0]==null) break;
              if (dataLine[0].startsWith("!--") && dataLine[0].length() > 3) {
                     dataBlockIsActive = false;
                     continue;
              }
else if (dataLine[0].startsWith("!--")) continue;
else if (!dataBlockIsActive && dataLine[0].length()==0) continue;                 
else dataBlockIsActive = true;               
             
if (dataBlockIsActive) {
                     String[] input = new String[inputColumns];
                     String[] expected = new String[expectationColumns];
                     System.arraycopy(dataLine, inputColumns, expected, 0,
                                  expectationColumns);
                     if (dataLine[0].length() > 0) {
                           if (j >= 0)dataList.add(j, td);
                            j++;
                           System.arraycopy(dataLine, 0, input, 0, inputColumns);
                           td = new DataBlock(i + 4, input, expected);
                     }
else if (td != null) {
                           td.addExpectedResult(expected);
                     }
              }
       }
      
       if (td != null) dataList.add(j, td);
       DataBlock[][] testData = new DataBlock[dataList.size()][1];
       for (int i = 0; i < dataList.size(); i++) {
              testData[i][0] = dataList.get(i);
       }
       return testData;
}


Use this link to get the full version of the TestDataProvider class:
Full version of the TestDataProvider class