Friday, May 18, 2012

First Forays into TDD

So on a recent project, I've finally gotten into Test Driven Development, and I have to say, I completely enjoyed it.

Without giving much away, the project is system allowing other internal business systems to publish XML content to subscribed users. Those users are then allowed to reply with specific responses based on the message content. The message has system-level content in a header, as well as business-level content, agnostic to the system. It became very obvious very quickly that the the concept of message threads would become important. Enter TDD...

The server-side was built using Enterprise Integration Pattern framework to abstract the boilerplate necessary to route messages back and forth, and down to a data store. So a message transformer was introduced to insert a ThreadID into the message header that correlated with the business system's SystemID and their provided ExternalID and ReferenceID. These latter ids, allow the business systems to track their messages in their own internal way. In this way an ExternalID + SystemID would map to a ThreadID in the system's internal cache. A follow-up message could then be given a new ExternalID pointing to a previous message through the ReferenceID.

Since I knew what the messages should look like coming in and going out, it can be modeled completely as a black-box, which naturally and happily lends itself to TDD (happily because it's the easiest case to get acquainted with TDD).

Given the predetermined inputs and outputs (an exception is a valid output, don't forget), I drew up some basic scenarios, and blew that up into a flowchart (via Gliffy):
From there, I was able to layout my general test cases. Many of them were just standard JUnit cases, like failing when not receiving an expected exception:
public void testReferenceIdNotCached() throws Exception {
  String inputXml = "<message><header><systemId>TestSystem</systemId><referenceid>ReferenceIdNotCached</referenceid></header><body /></message>";
  try {
    Message<String> output = transformer.transform(new Message<String>(inputXml));
                    "Value of <referenceid> field is not cached and should throw an exception. Output is::[%s].",output.getPayload()));
  } catch (MessageTransformationException e) {
    assert e.getCause() instanceof IllegalArgumentException : "Underlying exception is incorrect.";

Some of the tests though check that for the canned message inputted, creates an output message matching an expected format. This is where XmlUnit was a big help. It lets you compare XML strings, giving you the option to either expect an exact string or a similar string with a different ordering of elements. It also lets you go into a more detailed level where you can inspect the differences and potentially discard them, one by one. Below is an example of getting a similar output, since the ordering of the header elements doesn't matter, just that they exist, and the values are the same.

public void testRefExternalIdPassedWithRefCached() throws Exception {
  String referenceId = "EXT_" + UUID.randomUUID().toString();
  UUID threadId = threadIdCache.getNewAndPut(referenceId, "TestSystemId");

  String inputFormat = "<message><header><systemid>TestSystemId</systemid><referenceid>%s</referenceid></header><body /></message>";
  String inputXml = String.format(inputFormat, "TestSystemId", referenceId);
  Message<String> transformedMessage = transformer.transform(new Message<String>(inputXml));

  String expectedFormat = "<message><header><systemid>%s</systemid><threadid>%s</threadid><referenceid>%s</referenceid></header><body /></message>";
  String expectedXml = String.format(expectedFormat, "TestSystemId", threadId.toString(), referenceId);
  String transformedXml = stripMessageId(transformedMessage.getPayload());

  Diff diff = new Diff(expectedXml, transformedXml);
  assertTrue(diff.toString(), diff.similar());

Once these were written, I reused my flowchart to write the code. As a result of this coordinated effort, I immediately saw from an earlier version of the chart that I missed a couple of checks and that the ordering of them was out of whack, but my tests never really needed to change since my inputs and outputs were still the same. And since my tests were already written, I felt slightly compelled and more at ease to add more tests to make sure inputs existed and were in the correct format.

It was a great process, and I look forward to using it again in other parts of my code. The three things that really came across during this: it's very hard to go against habit making sure I write the tests before the code, I can see how difficult it can be to put in place in certain situations (like when refactoring), but you can eventually get tests in place on a large enough section of your code to make the process useful, and lastly, your tests are written!.

No comments:

Post a Comment