Data Driven Testing – Decoupling test data from test logic

Data-driven testing (DDT) is a term used in the testing of computer software to describe testing done using a table of conditions directly as test inputs and verifiable outputs as well as the process where test environment settings and control are not hard-coded. In the simplest form the tester supplies the inputs from a row in the table and expects the outputs which occur in the same row. The table typically contains values which correspond to boundary or partition input spaces. In the control methodology, test configuration is “read” from a database.

Anything that has a potential to change is separated out from the test logic and moved into an ‘external asset’. This can be a configuration or test dataset. The logic executed in the script is dictated by the data values. The script is simply a “driver” for the data that is held in the data source.

To convey the concept, I’ve created a sample project using TestNG. Pl. note implementation minimized for brevity. You will need following jars in your class path to successfully execute the project:

  1. poi-3.12-beta1-20150228.jar
  2. poi-ooxml-3.12-beta1-20150228.jar
  3. testng-6.8.21.jar
  4. xmlbeans-2.6.0.jar

The above were the versions which I had used for the demonstration. You could use the latest version of the above jars.

I’ve put the test data & expected outcome in an excel called TestData.xls and in my test case I refer it. The excel has multiple sheets which holds data for each test case type.

Below is the source code under test –

package com.sanjit;

/**
 * A simple minimal calculator. Implementation minmized for brevity.
 *
 * @author Sanjit Mohanty
 * @version 0.1
 *
 *
 * Revision History:
 * VERSION DATE AUTHOR COMMENT
 * 0.1 23-Apr-2015 Sanjit Mohanty initial create
 *
 *
 */

public class Calculator {

 public Calculator(){
 }

 public Double sum(Double obj1, Double obj2){
 return obj1+obj2;
 }

 public Double diff(Double obj1, Double obj2){
 return obj1-obj2;
 }

 public Double mul(Double obj1, Double obj2){
 return obj1*obj2;
 }

 public Double div(Double obj1, Double obj2){
 return obj1/obj2;
 }
}

Below is the unit test source code for the above class. Here we refer the excel for input data as well as for the expected outcome. No test data is mentioned here. The same test source runs for different inputs.

</pre>
<pre>package com.sanjit.test;

import java.lang.reflect.Method;

import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import com.sanjit.Calculator;
import com.sanjit.test.util.DataTypeParameter;
import com.sanjit.test.util.ExcelUtils;

/**
 * Test class for
 * {@link com.sanjit.Calculator}
 *
 * A simple example of Data Driven Testing!
 * Test data has been externalized to an excel and is being fed to the test case through TestNG's dataprovider.
 *
 * @author Sanjit Mohanty
 * @version 0.1
 *
 *
 * Revision History:
 * VERSION DATE AUTHOR COMMENT
 * 0.1 23-Apr-2015 Sanjit Mohanty initial create
 *
 *
 */

public class CalculatorTest {
 private Calculator calculator;

 @BeforeMethod
 public void initialize() {
 calculator = new Calculator();
 }

 /*
 * Data Provider feeding test data from an excel with multiple sheets for different test cases
 */
 @DataProvider(name = "testData")
 public Object[][] testData(final Method testMethod) throws Exception {
 DataTypeParameter parameters = testMethod
 .getAnnotation(DataTypeParameter.class);

 Object[][] testObjArray = ExcelUtils
 .getTableArray(
 "D://test//data//TestData.xls",
 parameters.testType()); // Edit the test data excel location accordingly

 return (testObjArray);

 }

 /*
 * Sum Test Case
 */
 @Test(dataProvider = "testData")
 @DataTypeParameter(testType = "SumTest")
 public void testSumChecker(String inputNumber1, String inputNumber2,
 String expectedResult) {
 Assert.assertEquals(
 Double.valueOf(expectedResult),
 calculator.sum(Double.valueOf(inputNumber1),
 Double.valueOf(inputNumber2)));
 }

 /*
 * Difference Test Case
 */
 @Test(dataProvider = "testData")
 @DataTypeParameter(testType = "DiffTest")
 public void testSubChecker(String inputNumber1, String inputNumber2,
 String expectedResult) {
 Assert.assertEquals(
 Double.valueOf(expectedResult),
 calculator.diff(Double.valueOf(inputNumber1),
 Double.valueOf(inputNumber2)));
 }

 /*
 * Multiplication Test Case
 */
 @Test(dataProvider = "testData")
 @DataTypeParameter(testType = "MulTest")
 public void testMulChecker(String inputNumber1, String inputNumber2,
 String expectedResult) {
 Assert.assertEquals(
 Double.valueOf(expectedResult),
 calculator.mul(Double.valueOf(inputNumber1),
 Double.valueOf(inputNumber2)));
 }

 /*
 * Division Test Case
 */
 @Test(dataProvider = "testData")
 @DataTypeParameter(testType = "DivTest")
 public void testDivChecker(String inputNumber1, String inputNumber2,
 String expectedResult) {
 Assert.assertEquals(
 Double.valueOf(expectedResult),
 calculator.div(Double.valueOf(inputNumber1),
 Double.valueOf(inputNumber2)));
 }
}

Below I’ve created a custom annotation to inject parameter into the TestNG data provider. In my case this annotation holds the test case type like SumTest, DiffTest, MulTest etc. Based on the test case type, the corresponding sheet in the excel is parsed by the test source code for the test run.

</pre>
<pre>package com.sanjit.test.util;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

/**
 * Custom annotation for parameter passing to the data provider.
 *
 * @author Sanjit Mohanty
 * @version 0.1
 *
 *
 * Revision History:
 * VERSION DATE AUTHOR COMMENT
 * 0.1 23-Apr-2015 Sanjit Mohanty initial create
 *
 *
 */

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataTypeParameter {
 String testType();
}

Below is a utility class for reading the excel sheet which has our test data as well as the expected outcome.

<pre>package com.sanjit.test.util;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.apache.poi.ss.usermodel.WorkbookFactory;

/**
 * Excel parsing utility class
 *
 * @author Sanjit Mohanty
 * @version 0.1
 *
 *
 * Revision History:
 * VERSION DATE AUTHOR COMMENT
 * 0.1 23-Apr-2015 Sanjit Mohanty initial create
 *
 *
 */

public class ExcelUtils {

 private static org.apache.poi.ss.usermodel.Workbook ExcelWBook;
 private static org.apache.poi.ss.usermodel.Sheet ExcelWSheet;

 private static org.apache.poi.ss.usermodel.Cell Cell;
 private static org.apache.poi.ss.usermodel.Row Row;

 public static Object[][] getTableArray(String FilePath, String SheetName)
 throws Exception {

 String[][] tabArray = null;

 try {

 int startRow = 1;
 int startCol = 1;
 int totalCols = 0;
 int totalRows = 0;
 int ci, cj;

 FileInputStream ExcelFile = new FileInputStream(FilePath);

 // Access the required test data sheet
 ExcelWBook = WorkbookFactory.create(ExcelFile);
 ExcelWSheet = ExcelWBook.getSheet(SheetName);

 totalRows = ExcelWSheet.getLastRowNum() + 1;
 if (totalRows >= 0) {
 totalCols = ExcelWSheet.getRow(0).getPhysicalNumberOfCells();
 }

 tabArray = new String[totalRows][totalCols];
 ci = 0;

 for (int i = startRow; i <= totalRows; i++, ci++) {

 cj = 0;

 for (int j = startCol; j <= totalCols; j++, cj++) {

 tabArray[ci][cj] = getCellData(i - 1, j - 1);

 System.out.println(tabArray[ci][cj]);

 }

 }
 }

 catch (FileNotFoundException e) {

 System.out.println("Could not read the Excel sheet");

 e.printStackTrace();

 }

 catch (IOException e) {

 System.out.println("Could not read the Excel sheet");

 e.printStackTrace();

 }

 return (tabArray);

 }

 public static String getCellData(int RowNum, int ColNum) throws Exception {

 try {

 Cell = ExcelWSheet.getRow(RowNum).getCell(ColNum);

 int dataType = Cell.getCellType();

 if (dataType == Cell.CELL_TYPE_NUMERIC) {
 return String.valueOf(Cell);

 } else if (dataType == Cell.CELL_TYPE_BLANK) {

 return "";
 } else if (dataType == Cell.CELL_TYPE_STRING) {

 String CellData = Cell.getStringCellValue();

 return CellData;

 }
 } catch (Exception e) {

 System.out.println(e.getMessage());

 throw (e);

 }
 return null;

 }
}

Mocking db calls in unit test

Here is my java class which is accessing DB to fetch movie names and then it validates if the fetched movie name has a movie called ‘Sangam’.

package com.sanjit;

import java.sql.*;

/**
* Class accessing the DB to fetch some data, which is then used for some
* validation
*
* @author Sanjit Mohanty
* @version 0.1
*
*          <pre>
* Revision History:
* VERSION  DATE           AUTHOR            COMMENT
* 0.1      21-Jul-2010    Sanjit Mohanty    initial create
* </pre>
*/

public class DBCallClass {

static Connection con;

public boolean isMovieSangam() throws SQLException {
ResultSet rs = fetchData();

while (rs.next()) {
String movieName = rs.getString("moviename");

if (movieName == "Sangam") {
return true;
}
}

return false;
}

public ResultSet fetchData() throws SQLException {

String connectionURL = "jdbc:postgresql://localhost:123456/movies;user=java;password=samples";

try {
Class.forName("org.postgresql.Driver");
con = DriverManager.getConnection(connectionURL);
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("select moviename from movies");

return rs;
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
con.close();
}

return null;
}
}

In order to unit test the above class, i’ve to mock the actual DB call and then return the test data. I’ve used powermock (with easymock api) library to achieve this.

So, the JUnit class for the above looks as below:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.easymock.EasyMock.expect;
import static org.powermock.api.easymock.PowerMock.*;

import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import com.sanjit.DBCallClass;

/**
* Example: Mocking the DB calls using powermock. Main Class:
* {@link com.sanjit.DBCallClass}
*
* @author Sanjit Mohanty
* @version 0.1
*
*          <pre>
* Revision History:
* VERSION  DATE           AUTHOR            COMMENT
* 0.1      21-Jul-2010    Sanjit Mohanty    initial create
* </pre>
*/

@RunWith(PowerMockRunner.class)
@PrepareForTest({ DBCallClass.class, DriverManager.class })
public class DBCallClassTest {

private String connectionURL;
private String sql;
private boolean isMovieSangam;

@Before
public void initialization() {
connectionURL = "jdbc:postgresql://localhost:123456/movies;user=java;password=samples";
sql = "select moviename from movies";
isMovieSangam = false;
}

@Test
public void testIsMovieSangam() throws Exception {

// Create mocks and set expectations
createMocksAndSetExpectations();

// Instantiate the class under test
DBCallClass dbCall = new DBCallClass();

// Invoke the method to test
isMovieSangam = dbCall.isMovieSangam();

// Verify the results
doVerification();
}

private void createMocksAndSetExpectations() throws Exception {

Connection mockConnection = createMock(Connection.class);
Statement mockStatement = createMock(Statement.class);
ResultSet mockResultSet = createMock(ResultSet.class);

// Mocking Static Class
mockStatic(DriverManager.class);
expect(DriverManager.getConnection(connectionURL)).andReturn(
mockConnection);
replay(DriverManager.class);

expect(mockConnection.createStatement()).andReturn(mockStatement)
.anyTimes();
mockConnection.close();
replay(mockConnection);

expect(mockStatement.executeQuery(sql)).andReturn(mockResultSet);
replay(mockStatement);

expect(mockResultSet.next()).andReturn(true);
expect(mockResultSet.getString("moviename")).andReturn("Sangam");
replay(mockResultSet);
replay(mockStatement);
}

private void doVerification() {
Assert.assertEquals(true, isMovieSangam);
}
}

To use powermock (with easymock api) in a maven project, include the following dependencies to the pom.xml file:

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.4.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-easymock</artifactId>
<version>1.4.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.0</version>
</dependency>

But if you are using a non-maven based java project then you need to place cglib-nodep-2.2.2.jar,
easymock-3.1.jar, javassist-3.16.1-GA.jar, powermock-easymock-1.4.12-full.jar, powermock-module-junit4-1.4.12.jar
and objenesis-1.2.jar in your classpath. Pl. note versions are just an indicator, you should use the latest
versions of these jars.