One of the things I got out of Agile Open California this year was a decision that I should work harder at removing database access from the unit tests for our Java code. It will probably be a pain, but I’ve dealt with legacy code before, I know the basic ideas of what to do, and reading Growing Object-Oriented Software introduced me to mocking frameworks, which seemed like a tool that might serve me well in this context. My best guess was that it would take me a day, plus or minus a factor of two, to get something in; and it shouldn’t take too many weeks (days, hopefully, if my next project is the right sort of thing) to for that to turn into a time savings.
So, once I’d made it through the accumulated post-conference tasks, I looked at the last set of unit tests that I’d written. And, actually, it looked like they’d be even easier to tame than I thought: I’d already put in a legacy code barrier by writing the new functionality there as a static method, so I didn’t even have to instantiate an instance of the class that I was allegedly testing, just instances of the arguments to the method in question.
Looking more closely, it turned out that there was really only one argument that I needed to worry about. It was an instance of a concrete class that was a pain to instantiate without running through all of our Spring machinery: it created a static Memcache object, it had another static instance variable that was looking up a Spring-initialized bean (that eventually depended on grabbing some data configuration from a static data file), etc. I certainly know techniques for delaying static initialization, and thought about going down that path for a minute, but then I stopped myself and said: I want to start using jMock for the heavy lifting of instantiation, so I shouldn’t go that route at all!
That means that I need an interface instead of a concrete class; is that okay here? I looked at the test and the code under test, and they weren’t about that messy concrete class, I was just testing something that was using it in a not-very-deep manner. Given that, I didn’t see any reason why instantiating an interface instead would weaken my test. I didn’t have an interface handy, so I created one for the concrete class to implement; I thought for a couple of minutes about what it should look like before realizing that I didn’t actually care right then, what it needed to look like was (at first) enough to get the test in question to compile! That bit of test and product code called a grand total of two methods of the original class; I added those two methods to the interface, and looked up the jMock boilerplate for instantiating an instance of that interface; and poof, my tests were compiling, without any database-dependent code involved!
Of course, they weren’t passing yet: assertions were failing, and jMock was also informing me that mocked out methods were called. I looked a little more closely, and realized that one of the two methods in question was a setter that (in the context of running these tests) was only being called to set up the object so that the other method would return what I wanted. Which makes sense in a non-mock context, but in a mock context, you can just directly tell the method to return what you want. So I deleted that setter from the interface and from the test code, told the test’s Mockery to return some appropriate data when the other method was called, and ran the tests.
And they passed! Indeed, they passed so quickly that, at first, I assumed things had gone wrong: the compilation still took a few seconds, but once the tests started running, IntelliJ didn’t even have time to show me the listing of all of the tests that it was running before they all finished. But I did some poking around, adding assertion failures or commenting out lines of product code, and the tests were indeed all running, they were just running approximately 250 times as fast as they had been before.
Which was a huge success! And not only were the tests fast to run, they were also fast to write. I’d estimated that this would take a day or so; in fact, it took somewhere between half an hour and an hour for me to get this working, despite my complete prior lack of jMock experience. Admittedly, I’d lucked out by picking a clump of tests that was particularly easy to convert, but still: my tests were running hundreds of times faster after less than an hour of work! Amazing.
Flush from that success, I used jMock for the next feature that I was working on. That feature involved bringing some (actually pretty well written) legacy code under test, doing some replumbing to make it more extensible, and then extending it slightly. There were several more function arguments to deal with here, but all but one of them was already an interface. The one exception was the class that I’d just started to convert to an interface; so my first step when bringing each chunk of code under test was to try to replace the class with the interface when it showed up as an argument in that chunk of code, see what the compiler complained about, pull those methods up to the new interface, and repeat. (And if it got too thorny and if I was in a code branch that I wasn’t going to reach with my test, I’d just throw in a cast to silence the compiler.)
Then, once I’d gotten it to compile, I’d create a unit test that instantiated mock objects and called the method. This would immediately point out a calls that I needed to tell the Mockery to expect; that’s fine, I could iterate on that every 30 seconds, so even in the most complicated case, I had the test running in under five minutes, and passing for the right reason a minute after that. The upshot was that, within a few hours, I had all of the relevant code running with tests that were good enough to support the refactoring I wanted; a few hours later, the first refactoring was done, and I was in a good TDD flow for the first time since I’ve started my Java work on that project. A couple of days later I had the new functionality in place, running correctly in the first end-to-end try, with the total number of unit tests on the project increased by 50 percent or so, and with a structure in place so that will enable us to knock off a bunch of upcoming feature requests in an hour or two each.
I am, obviously, very pleased with the results, and very impressed with jMock: it’s amazing how much work it’s been saving me. So I’m a total convert to using it to tame legacy code; what I’m interested in next is how to use it, as Growing Object-Oriented Software suggests, to guide the development of new interfaces going forward. (It pulled out an interface for me in the code I was working with last week, but it was an ugly interface, serving more to point out warts in the existing structure than anything else.) Lots of fun, I look forward to the next step in that journey.
- October 26, 2010 @ 07:02:52 [Current Revision] by David Carlton
- October 25, 2010 @ 21:37:55 by David Carlton