Moving Away from Exceptional Code
14 June, 2009 § Leave a comment
I once took a training course taught by Ron Jeffries and Chet Hendrickson on Test Driven Development. One of the participants in the course asked both Ron and Chet what are their recommendations on testing exceptional code paths. At first I didn’t think much of the question, since all the unit testing frameworks I’ve used have built-in ways to test exceptional code paths.
- VSTS Unit Testing (.NET) has an attribute that you can put on a test to say that it is expecting an exception:
[TestMethod, ExpectedException( typeof ( ApplicationException ) ) ] public void CreationOfFooWithNullParameterShouldThrowApplicationException() { var foo = new Foo( null ); //should throw an ApplicationException }
- Google Test (C++) places a macro around the method call that is expecting an exception to be thrown:
ASSERT_THROW(Foo(5), bar_exception);
- unittest (Python) has a special assert to test if an exception was raised, similar to Google Test:
def testsample(self): self.assertRaises(ValueError, random.sample, self.seq, 20)
The answer that Ron gave wasn’t expected, but led me to think about the way that we use exceptions within our codebase.
From what I remember, Ron said that he simply doesn’t test exceptional cases. How? He doesn’t have exceptional cases. He recommended using the Null Object pattern to get away from most of the exceptional cases.
Developers often have to pollute their code with checks to see if a value is null, like so:
XmlNode* userNode = xmlParser.GetNode( "user" ); return ( userNode == NULL ) ? "" : userNode->Text();
In the case above, the call to GetNode would search the XML document for a node with a tagName of “user”. If it finds it, it will return a pointer to the data. If not, then it will return NULL. This makes a lot of sense to developers, but it ruins some of the value of abstraction and good object-oriented programming.
First, any client who makes this call to GetNode has to know how GetNode works (they have to know that GetNode will return NULL if it can’t find the node). Second, there is a type check going on here, basically a runtime check to see if the type returned from GetNode is an actual XmlNode or if it is not.
One way to refactor away from this is to use the Null Object pattern. Martin Fowler talks about the Null Object pattern in his book, Refactoring: Improving the Design of Existing Code. In the section entitled, Introduce Null Object, there is a story told by Ron Jeffries describing how he has used Null Object to treat missing objects as actual objects.
The story goes over how if a record didn’t exist in the database, they would return an object like MissingPersonRecord, which would be derived from PersonRecord. All calls to methods on a MissingPersonRecord would simply return some default behavior and not complain. For example, if the MissingPersonRecord is asked for their name, they would respond with a blank string.
This allows any caller of the methods to not have to worry about missing people in the database. See how the code has changed:
XmlNode* userNode = xmlParser.GetNode( "user" ); return userNode->Text();
There is now no change in behavior if the node doesn’t exist, and there is also no knowledge about how GetNode works (besides the fact that the caller knows that it will never return NULL 😉 ).
One library that I have used that follows this pattern is the TinyXml library in C++. When a caller asks for an XML node, there is no checking to see if that node really exists before making the call. This allows you to write code like so:
TiXmlNode* userNode = xmlParser.GetNode( "user" ); return userNode->FirstChild()->FirstChild()->Text();
A disadvantage to this is that when a Null Object has to be used, the error in the code can take a while to find. You may not know that there is a problem until a user object is displayed on screen without a required field like a “name”.
To solve this, you would have to debug and find the Null Object to determine why it is not a real object. Another approach could be to have the Null Object log a message in the Event Log when specific methods are called on it, but this can create too much noise, especially if you are using the Null Object pattern in the way that TinyXml does.
So, that is a little introduction on the Null Object pattern. To summarize, you can use the Null Object pattern to improve abstraction and polymorphism, and to reduce the lines of code per method.
My notes on “Test Driven Development: By Example” by Kent Beck
24 April, 2009 § 2 Comments
These are my unordered notes that I took while reading Test Driven Development: By Example, by Kent Beck. I took a different route with these notes as opposed to my previous notes on Effective C++. If you read both, let me know which way is more informative.
Test Driven Development: By Example is a book that teaches how you can test the software that you write proactively. Ron Jeffries said that the goal of TDD is “clean code that works.” When you write your code TDD, the tests tell you that it works, and since one of the steps of TDD is to refactor, you should have clean code that lacks duplication.
The book is set up in three distinct parts. The first part takes you through a simple currency exchange example and shows how you could write unit tests, starting with something that is achievable and building up from there. The second part explains how xUnit works, and interestingly, Kent shows how by having you write your own xUnit library in Python using TDD. Writing the testing library using tests? Yes, and it’s really cool how he does it. Part three covers many TDD design patterns and development processes. Here are the notes that I took while reading the book:
- While coding, you should make a to-do list. Tackle what is easy and if you notice anything else to do, just add it to the to-do list. You can come back to the list later for the next item to work on. This way, you won’t forget anything that comes to mind and can stay on track. Your to-do list can also be a list of tests that need to be made. These tests will then lead development of the feature.
- With TDD, you can test an experiment and back it out. You know right away if it will work or not.
- Three strategies for a green bar:
- Fake It – Return a constant
- Obvious Implementation – If the implementation is obvious, go ahead and implement it
- Triangulation – Two tests that both cover the same code to get rid of faking it. If you are having trouble figuring out how to implement the code, triangulation can help you out.
- Obvious implementation is a fast way to implement what you know. Triangulation and Faking It are baby steps compared to Obvious Implementation.
- Value objects: once initialized, they don’t change. Anything that changes them returns a copy with the new value. Value objects must also implement operators like equals though.
- Impostor pattern: Objects that have the same external protocol but don’t behave the same way.
- Chapter 14 talks about creating a pair class. Isn’t there something like std::pair in Java?
- Bill Wake says your tests should follow the three A’s: { Arrange, Act, Assert }
- In the same spirit of test first, you should write assertions first and then figure out what test code is needed.
- Don’t obscure the test results, use Evident Data to show what’s expected. This means that if a test is checking that a result is 2/3 the input, you shouldn’t precalculate the expected output. Instead, you should just show that the output is expected to be 2/3 * input.
- If a test needs to work on many values, start with just one, then refactor to many.
- You should end up with the same number of lines of test code as model code with TDD. This doubles the amount of code that you write, but it should limit the amount of regression that occurs, and give you an idea right away if your code works as intended.
- The number of changes per refactoring should have a fat tail, this is also called a Leptokurtotic profile.
Notable quotes:
- On xUnit, “Never in the annals of software engineering was so much owed by so many to so few lines of code” – Martin Fowler
- On TDD: “The goal is clean code that works” – Ron Jeffries