Tests are a safety net when the circus is in town. Make the net wide without adding more rope than you need.
Why I stopped copy pasting tests and slept better
This week I stared at a wall of green JUnit bars and felt nothing. The suite was huge and yet I knew it was mostly copy paste. Same test repeated with different inputs. Same flakiness when a helper changed. I have been living in both worlds lately. JUnit 3 on older projects with that familiar extends TestCase vibe and JUnit 4 with annotations on newer code targeting Java 5. The leap is real. Annotations clean up so much. No more test prefixes. Before and After make more sense than setUp and tearDown incantations.
On the tools side the mood is bright. Eclipse Callisto hums along nicely. IntelliJ is a joy. NetBeans is winning fans again. Java 6 just dropped and feels snappy. Windows Vista is fresh out of the oven if you are brave. Apple teased a phone that looks like a slab of glass. Feeds buzz with Rails releases and Yahoo Pipes and everyone is glued to Digg. In the middle of that noise I want something simple that pays rent every day. Fewer tests to maintain. More cases covered. Less yanking of cables when the build breaks.
The moment parameterised tests clicked
At lunch I pulled a tiny refactor on a helper that validates dates. Then came the cruel part. I had twenty tests saying the same thing with different numbers. Tweaked a condition and half the suite turned red. The fix was two lines and a lot of scrolling. That is when I dusted off the JUnit 4 parameterised runner. A small feature that flips how we write test data. Move the inputs into a table. Keep the assertion once. Let the runner loop for you.
I rewrote that class with a data table and a single assert. The suite ran in the same time. Coverage jumped a bit because I felt encouraged to throw in more edge cases. The class got shorter and easier to scan. It felt like cheating in the good way.
Deep dive 1. JUnit 4 parameterised tests step by step
If you are already on JUnit 4 you can use the built in runner today. You need Java 5 since it uses annotations and generics. Here is a tiny production class we want to test. The classic leap year rules make a neat example.
public final class LeapYear {
private LeapYear() { }
// True if year divisible by 4 except centuries unless divisible by 400
public static boolean isLeap(int year) {
if (year % 400 == 0) return true;
if (year % 100 == 0) return false;
return year % 4 == 0;
}
}Now the parameterised test. The idea is simple. You declare the runner, supply a static method that returns a collection of input rows, and receive those values in your test class constructor. Each row makes the runner execute your test methods once.
import static org.junit.Assert.*;
import java.util.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class LeapYearTest {
private final int inputYear;
private final boolean expected;
public LeapYearTest(int inputYear, boolean expected) {
this.inputYear = inputYear;
this.expected = expected;
}
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
// year, isLeap
{1600, true},
{1700, false},
{1996, true},
{1999, false},
{2000, true},
{1900, false},
{2004, true},
{2001, false}
});
}
@Test
public void leap_year_rule_applies() {
assertEquals("Year " + inputYear, expected, LeapYear.isLeap(inputYear));
}
}What just happened
- @RunWith(Parameterized.class) tells JUnit to use the special runner.
- @Parameterized.Parameters marks a static factory that returns a collection of Object arrays. Each inner array is a row of constructor arguments.
- The runner creates an instance per row and calls your test methods for each instance.
That is it. One assertion, eight cases. Add more rows as you learn, not more methods. Green bars get more interesting when they represent many surprises waiting to happen.
Deep dive 2. Build strong datasets and keep them readable
Parameterised tests shine when the data tells a story. A few tips from the trenches.
- Start with boundaries. For leap years I began with 1600 and 2000 to prove the 400 rule, then 1700 and 1900 to prove the century exception, then random years around switching points. For strings think empty, one char, long, unicode, and nasty whitespace.
- Keep rows short and aligned. Two or three columns are easy on the eyes. Resist packing an entire scenario into a single row. If you need many fields consider a value object and pass that instead.
- Name values in the code. The assert message I used includes the year. You can go further and include case labels in the expected side. When it fails on the build server you want to see which row broke without opening an IDE.
- Grow the table during bug fixes. Every time a bug escapes, add a row that reproduces it. Your future self will thank you.
Sometimes your data lives outside Java. A small CSV works for long lists like country codes or currency formats. You can still feed that into the parameterised runner with a tiny helper. Keep I O light to avoid slowing the suite.
@Parameterized.Parameters
public static Collection<Object[]> data() throws Exception {
List<Object[]> rows = new ArrayList<Object[]>();
InputStream in = LeapYearTest.class.getResourceAsStream("/leap-data.csv");
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String line;
while ((line = br.readLine()) != null) {
if (line.trim().isEmpty() || line.startsWith("#")) continue;
String[] parts = line.split(",");
rows.add(new Object[] { Integer.parseInt(parts[0].trim()),
Boolean.parseBoolean(parts[1].trim()) });
}
br.close();
return rows;
}Example CSV living under src test resources.
# year,expected
1600,true
1700,false
1996,true
1999,falseThat lets non Java teammates contribute test cases. Keep it simple. No need for a full parser unless you enjoy weekend projects.
Deep dive 3. Pitfalls and simple fixes
First time I used the runner I stumbled into a few gotchas. Nothing tragic. Worth calling out.
- State per row. JUnit creates a new instance per row. Your fields start clean for each case. Keep shared state in static fields only if it is truly read only. Better, avoid shared state at all.
- One class multiple methods. You can have several test methods and they will all run for each row. That is nice when you want to assert a few related things on the same inputs. If methods feel unrelated, split the class and keep focus tight.
- Assertion messages matter. Since each failure looks the same in the method name, write messages that include the inputs. You saw the “Year X” trick above. This pays off in Maven Surefire logs and in CI like CruiseControl or Continuum.
- IDE runners. Eclipse and IntelliJ both run parameterised tests fine. In Eclipse you will see a class node with many children. If it feels odd at first, remember each child is a row.
- Maven and Ant. Surefire picks them up like any other test if your naming pattern matches. No special plugin needed. Ant JUnit task works too. Keep your dependencies tidy and make sure the JUnit 4 jar is on the test classpath.
- Migration from JUnit 3. You cannot mix the new runner with extends TestCase. Move the class to pure JUnit 4 style. No inheritance, use annotations, static imports for asserts. It is a clean break and worth it.
Bonus. A quick contrast with TestNG
Some teams I know run TestNG with DataProviders. The idea is similar. Supply data and the framework loops. If you are already in JUnit 4 land and do not want an extra dependency, parameterised tests get you far. If you need more advanced features like grouping and suite control, TestNG has that baked in. Pick one and stay consistent inside a project.
Reflective close
We are all trying to ship without breaking stuff. Parameterised tests let you cover more with less. They push you to think in tables and boundaries. They turn a dozen nearly identical methods into one clear assertion with many rows of truth. That is good for the code and good for the head.
The tools are ready. JUnit 4 runs smooth on Java 5 and up. IDEs are friendly. Builds are happy. The web moves fast but the lesson here is quiet. Gather examples. Write them down. Let the runner do the loops. As your datasets grow, your confidence grows with them, without the noise of extra boilerplate.
If you have a brittle suite today, pick one class and try this trick. Grab a function you know well. Write the cases first in a small table. Watch green bars carry a little more meaning. That is the feeling I chase when I hit Run. Fewer tests to read. Many more cases covered. A net wide enough for a messy day.