lysergicjava

oh, technology

Menu Close

Month: October 2010

Feeding TestNG data providers (stupid TestNG tricks volume 1)

TestNG is great.  It’s one of my favorite tools.  Beneath the simple facade lurks a creature of flexibility and power.  That being said, every tool has its limitations, and when TestNG doesn’t give me what I want out of the box, which is rarely, I go poking around looking for ways to game the system.  Here’s one.

Data providers are pretty sweet.  They can feed a test method with a wide variety of data to run against, and the framework even reports the arguments the provider passed in, making forensics easier.  There are limitations in their design which can make them clunky to use in certain situations, though.  Chief among these is that the data providers themselves don’t have an easy interface to accept arguments.

Why, you may ask, would a data provider need to accept arguments?  It’s a fair question, seeing as how the data provider’s responsibility is to dole out arguments.  Turns out there are good reasons.  For instance: I had created a data provider that reads through a file and sends each line as an argument to a test method.  Fairly straightforward stuff; but my first implementation had the provider reading the same file every time.  The file name was hardcoded in the provider.  What I wanted was a way to have a single provider read any arbitrary file, and what’s more, to have the test method itself tell the provider where its data could be found.  Code reuse, yah?

Problem is, data providers can’t take arbitrary inputs.  They can, however, accept a reference to the current test method.  So, with this chink in the armor, I created a new annotation, DataProviderArguments, which I could attach to a test method:

package com.netflix.systemtests.api.commons.dataproviders;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
* Annotation for feeding arguments to methods conforming to the
* "@DataProvider" annotation type.
* @author jharen
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface DataProviderArguments
{
  /**
  * String array of key-value pairs fed to a dynamic data provider.
  * Should be in the form of key=value, e.g., <br />
  * args={"foo=bar", "biz=baz"}
  */
String[] value();
}

Next, I wrote a helper class to unravel the test method and extract the arguments:

package com.netflix.systemtests.api.commons.dataproviders;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class DataProviderUtils
{
	protected static Map<String, String> resolveDataProviderArguments(Method testMethod) throws Exception
	{
		if (testMethod == null)
			throw new IllegalArgumentException("Test Method context cannot be null.");

		DataProviderArguments args = testMethod.getAnnotation(DataProviderArguments.class);
		if (args == null)
			throw new IllegalArgumentException("Test Method context has no DataProviderArguments annotation.");
		if (args.value() == null || args.value().length == 0)
			throw new IllegalArgumentException("Test Method context has a malformed DataProviderArguments annotation.");
		Map<String, String> arguments = new HashMap<String, String>();
		for (int i = 0; i < args.value().length; i++)
		{
			String[] parts = args.value()[i].split("=");
			arguments.put(parts[0], parts[1]);
		}
		return arguments;
	}
}

And with that, my FileDataProvider can read any given file:

package com.netflix.systemtests.api.commons.dataproviders;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.testng.annotations.DataProvider;

public class FileDataProvider
{
	@DataProvider(name="getDataFromFile")
	public static Iterator<Object[]> getDataFromFile(Method testMethod) throws Exception
	{
		Map<String, String> arguments = DataProviderUtils.resolveDataProviderArguments(testMethod);
		List<String> lines = FileDataProvider.getRawLinesFromFile(arguments.get("filePath"));
		List<Object[]> data = new ArrayList<Object[]>();
		for (String line : lines)
		{
			data.add(new Object[]{line});
		}
		return data.iterator();
	}

	public static List<String> getRawLinesFromFile(Method testMethod) throws Exception
	{
		Map<String, String> arguments = DataProviderUtils.resolveDataProviderArguments(testMethod);
		return FileDataProvider.getRawLinesFromFile(arguments.get("filePath"));
	}

	@SuppressWarnings("unchecked")
	public static List<String> getRawLinesFromFile(String filePath) throws Exception
	{
		InputStream is = new FileInputStream(new File(filePath));
		List<String> lines = IOUtils.readLines(is, "UTF-8");
		is.close();
		return lines;
	}
}

And now, my test methods can specify what file to fetch their data from:

@Test(dataProviderClass=com.netflix.systemtests.api.commons.dataproviders.TestAccountDataProvider.class, dataProvider="getTestAccountsFromFile")
@DataProviderArguments("filePath=src/main/resources/input-files/testAccounts.txt")
public void fetchInstantQueueAsJSON(String email, String accountID) throws Exception
{
  // blah blah blah testy test goes here
}

For my next trick, I think I’ll work on getting TestNG TestFactories to play the same game.  Then it’ll get interesting.  Till then, keep your bars green and your code clean.  Happy testing!

JPath 1.0 released

Ask anyone who’s had to work with JSON in Java: consuming, querying, and validating JSON objects in Java is a tedious chore. Either you’re stuck writing structured beans mapped to JSON responses, which is time-consuming and brittle, or you need to resort to circumlocutious chains of get() calls with typecasting (or foreknowledge of types) at every step. In particular, there is no equivalent (or even approximation) of XML’s XPath — until now.  JPath allows for concise, flexible navigation and validation of JSON data.  Check it out over at BLC.