Thursday, July 23, 2009

Groovy+Testing: Little Things, Big Impact

When it comes to unit testing, it's the little things that drive me nuts. Or maybe it's the little things that make testing enjoyable. Optimism is better, I suppose. Now I don't buy into all that tipping point garbage, but there are two Groovy features that I miss immediately when returning to Java. Almost every other rising language has them, they seem like trivialities to add to a language, and they aren't likely to be in Project Coin. (Beware, all 3 of the previous statements are suppositions). And these two features are... multi-line strings and default parameter values.

As ridiculous as it sounds, I'm going to spend several paragraphs extolling the virtues of two language features that are so simple they can be explained in a single sentence each.

Multiline Strings

Why am I constantly testing with XML snippets? This is a question I don't often ask myself in Groovy because XML snippets look really nice embedded in a test method:

def garage = new Garage(
new Car(CAR_NAME1, CAR_MAKE1),
new Car(CAR_NAME2, CAR_MAKE2),
new Car(CAR_NAME3, CAR_MAKE3)
)

def actual = serialize(garage, Garage)

def expected = """
<garage>
<car name="$CAR_NAME1" make="$CAR_MAKE1" />
<car name="$CAR_NAME2" make="$CAR_MAKE2" />
<car name="$CAR_NAME3" make="$CAR_MAKE3" />
</garage>"""

assertXmlEqual(expected, actual)

This test method clearly shows a garage full of cars being created and a garage full of cars being expected. There is symmetry and conciseness, and I find it pretty easy to see how the input relates to the expected output. Compare the Java version:
Garage garage = new Garage(
new Car(CAR_NAME1, CAR_MAKE1),
new Car(CAR_NAME2, CAR_MAKE2),
new Car(CAR_NAME3, CAR_MAKE3)
);

String actual = serialize(garage, Garage.class);

String expected =
"<garage>" +
" <car name=\"" + CAR_NAME1 + "\" make=\"" + CAR_MAKE1 + "\" /> " +
" <car name=\"" + CAR_NAME2 + "\" make=\"" + CAR_MAKE2 + "\" /> " +
" <car name=\"" + CAR_NAME3 + "\" make=\"" + CAR_MAKE3 + "\" /> " +
"</garage>";

assertXmlEqual(expected, actual);

Whee! Escaping quote characters is fun! I can't see how the expected block lines up with the input anymore, it's lost in a sea of accidental complexity, back slashes, and quotes. Yuck. And this is some pretty simple XML. The tests as documentation idea starts to suffer without multiline strings because the only thing that finds this format easy to read is the compiler. Speaking of documentation, how about copy and pasting the XML snippet into a user document or email? Or heaven forbid generating user documentation off the test case! You're just not going to do that with the Java version. In fact, I'd say most the time I don't even write the Java version like this... the escapes and quotes bring so much clutter that I just store XML snippets in separate XML files and read them in during each test:
Garage garage = new Garage(
new Car(CAR_NAME1, CAR_MAKE1),
new Car(CAR_NAME2, CAR_MAKE2),
new Car(CAR_NAME3, CAR_MAKE3)
);

String actual = serialize(garage, Garage.class);

String expected = readFromFile("MyTest.sample.xml");

assertXmlEqual(expected, actual);

It's no longer abrasive on the eye, but you also completely lose the ability to see the relationship between the input and the output. You aren't helping the tests document the system by hiding all the input from the reader!

Such a small thing and I find it so valuable, and it's nothing more than a notational convenience, a little syntactical sugar.

Default Parameter Values
Tiny feature; big effect on unit testing.

Unit testing is about building tons of data, all in different configurations or states of construction, and then throwing that data at the system under test, observing and verifying the results. Unit tests look a lot different from production code. In production code, subroutines exist to perform smaller pieces of a larger, decomposed problem. In test code, subroutines exist to build objects. The design principles between production and test are different, and it makes sense that some language features might be very useful in test but of more limited value in production. I find default parameter values fit within this category. Consider a set of test methods that need to build a login form (username, password fields) in different states:
JFrame frame1 = makeLoginWindow()
JFrame frame2 = makeLoginWindow("some username")
JFrame frame3 = makeLoginWindow("some username", "some password")

Making a function to build a Login Window in these various states can be done within a single function in Groovy, you just have to use default parameters.
private JFrame makeLoginWindow(String username = null, String password = null) {

JTextField userField = new JTextField()
JTextField passwordField = new JTextField()
if (username != null) userField.text = username
if (password != null) passwordField.text = password

JFrame frame = new JFrame()
frame.contentPane.add(userField)
frame.contentPane.add(passwordField)
return frame
}

Under the covers, this is still an overloaded method to the JVM. But that's alright. There is a single source for all the test data configuration, and navigating (ie reading) the test case is made easier by having a single source.

In a Java test case, you're stuck defining at least 3 methods. The real rock star programmers are going to chain the method calls together... ohhh:
private JFrame makeLoginWindow() {
makeLoginWindow(null, null);
}
private JFrame makeLoginWindow(String username) {
makeLoginWindow(username, null);
}
private JFrame makeLoginWindow(String username, String password) {

JTextField userField = new JTextField();
JTextField passwordField = new JTextField();
if (username != null) userField.setText(username);
if (password != null) passwordField.setText(password);

JFrame frame = new JFrame();
frame.contentPane.add(userField);
frame.contentPane.add(passwordField);
return frame;
}

Conceptually, we've just added 3 entities to the test case that didn't exist before. Navigating around labyrinths of production code is a necessary evil. There are other design constraints at play and stuffing everything into one place isn't always the right thing to do. But in the test tree, labyrinths of chained code are just evil, nothing necessary about it. Having to chase down dependencies undermines one the chief principles of testing: show clearly and simply how input is transformed to output. Default Parameters for the win.

Sadly, when advocating using Groovy as a testing language, these two features are unconvincing evidence of Groovy's superiority. They just don't seem like much. They aren't very exciting. So why are they the first things I miss when creating new Java test cases?

By the way: PHP has both these features. Ha!

3 comments:

Dave Newton said...

It's only marginally better, but using String.format avoids ugly String concatenation.

Hamlet D'Arcy said...

@Dave agreed, I use String.format all the time. But it always results in me futzing with spacing a line breaks to try and make it look readable.

Walter Harley said...

Not only do chained methods add more to the tests, they add more to the call stack when debugging. And the only way to truly be sure what the chained methods do is to inspect their implementation. Frankly, rather than adding a bunch of "convenience methods", I tend to force the caller to explicitly specify the values.

Default values are reasonably self-documenting and they live where they ought to, in the method specification.