Monday, June 29, 2009

Shut up and write those tests

One of the things I love about my new job is that this team is committed to the craft of software development, and is introducing me to Deep Testing.  I was pointed to this excellent book, Working Effectively with Legacy Code by Michael Feathers, and to this amazing little toolkit called Mockito.


Working Effectively with Legacy Code assumes a Test-Driven Development attitude and philosophy, and then tells you how to go into a hairball of existing code and get it to be testable.  Chapter titles include "I Can't Get This Class into a Test Harness" and "I Don't  Understand the Code Well Enough to Change It," and includes techniques for breaking that bugaboo of unit testing, hard-coded dependencies, like "Extract and Override Method Call," "Introduce Instance Delegator" and "Subclass and Override Method."  Excellent stuff.

Mockito is a mocking framework that is just insanely easy to use.  Here is a piece of code I wrote where the class to test had a large number of dependencies, and I just mocked out all but the relevant ones.  This took me about ten minutes to write, and it was an excellent way to test impact of the changes I was making:

Checkpointable mockCheckpointable = mock(Checkpointable.class);
CrawlerContent mockCrawlerContent = mock(CrawlerContent.class);
List crawlerItems = new ArrayList();
crawlerItems.add(mockCrawlerContent);

ContentStream mockContentStream = mock(ContentStream.class);

when(mockCheckpointable.getItems()).thenReturn(crawlerItems);
when(_mockCrawlerContent.getContentStream()).thenReturn(mockContentStream);

_messageManager.sendCheckpoint(mockCheckpointable);

verify(_mockContentBuffer).handleContentFromCrawler(mockContentStream, mockCrawler);
verify(_mockWalkUpdater).handleCheckpoint(mockCheckpointable);

With tools like this in hand, plus an attitude of commitment to excellence, I now firmly believe you can wrestle your code to the ground and get it into a unit test framework and start improving not only the testability and quality but overall architecture of your code.  I don't care if your code is full of massive, crufty code you can't understand, with lots of accesses to the network and the database, you can write unit tests that test modules independently. 

So shut up and write those tests.  You have no excuses.

6 comments:

Rogerio said...

That is a great book, but it's five years old. Things have changed since then.

Ideally, refactoring of code in use should only be done with a good test suite already in place. Back then, this was not possible because of limited testing tools.

Today, any code is testable, if the proper mocking tool is used. You can leave the "hairball of existing code" as is initially, write all the unit tests you want, and then start refactoring to improve the design. In the end, you won't even have to refactor as much, since it's not necessary to work around limitations of the mocking tool.

The tool I am talking about is JMockit.

Using it, your example code would look like this:

public void testSendCheckPoint(
final Checkpointable mockCheckpointable,
final CrawlerContent mockCrawlerContent, final ContentStream mockContentStream)
{
final List crawlerItems = new ArrayList();
crawlerItems.add(mockCrawlerContent);

new NonStrictExpectations() {{
mockCheckpointable.getItems(); returns(crawlerItems);
mockCrawlerContent.getContentStream(); returns(mockContentStream);
}};
_messageManager.sendCheckpoint(mockCheckpointable);

new Verifications() {{
_mockContentBuffer.handleContentFromCrawler(mockContentStream, mockCrawler);
_mockWalkUpdater.handleCheckpoint(mockCheckpointable);
}};
}

David Van Couvering said...

Hm, the book says the same thing you do: only refactor with a good test suite already in place. Some of the techniques have changed, but the principle is the same.

For example, you can't inject mock objects if the objects are created inside the constructor. You can't add a test for certain behavior that you want to refactor if that behavior is embedded in a big method. The trick is how to write tests *before* you refactor, and that's what this book is all about.

BTW, IMHO the Mockito syntax is more readable than what you showed me with JMockit.

jlorenzen said...

I agree, the jmockit syntax is too verbose.
I continue to hear good things about mockito, but I think I still prefer testing java code using groovy. Groovy provides several methods to create mocks including metaClass. IMO it even more readable and easier than other java mocking frameworks. Read here for more: http://jlorenzen.blogspot.com/2009/04/mock-testing-with-groovy.html

David Van Couvering said...

Groovy looks nice, but it sure looks like you can do all this with Mockito. In general it's easy to write simple "thenReturn" kind of statements, but you can have a mocked routine run an entire method that does whatever you want if you so desire.

jlorenzen said...

Didn't know that.
One nice thing that I found groovy can do was mock out static method calls like Thread.startDaemon(), which I needed on a recent grails project I was working on.

Thread.metaClass.static.startDaemon = {Closure c -> c.call()}

Franz See said...

I'd just like to share my experience with mocking...

A problem with the use of mocks is that it's not easy to read (even with Mockito). Sure, you can cleanly state your fake behaviors individually, but I've yet to find a way to make it naturally fit a test structure (I mean, as part of the test, what does it all mean?).

For example, in your test, sure you've managed to nail down _messageManager.sendCheckpoint(...)'s behavior, but the test falls short as a specification. And if your test is hard to understand, other developers might not bother fixing it properly once it breaks.

And to make matters worse, mock-base tests tend to be brittle. Thus if your mock-base test breaks, it might not necessarily mean that the functionality broke. It could simple mean the implementation change though the output/behavior is still the same (and it happens more often than you think..which may lead you to loosen up the expectations..which may in turn fail to capture scenario ....it's a give & take).

Furthermore, mock-base characterization tests does not offer much guarantee in terms of refactoring existing code (since it is coupled with specific implementation).

..Anyway, if anybody knows how to cleanly right mocks (as part of the whole test), and in a matter that it does not cry wolf, I'm all ears.