While on my grand European trip, I stopped in for a day at a nice company with a fun group of people doing good work on a legacy code base. They challenged me to improve an existing test using mocks. The test was typical of those I’ve seen in legacy code situations: there was a whole lot of setup code because you couldn’t instantiate any single object without instantiating a zillion of them, and the complexity of the test made figuring out its precise purpose difficult.
After some talk, we figured out that what the test really wanted to check was that when a Quote is recalculated because it’s out-of-date, you get a brand-new Quote.
Rather than morph the test, I tried writing it afresh in my mockish style. A lot of the complexity of the test was in setting things up so that an existing quote should be retrieved. Since my style these days is to push off any hard work to a mocked-out new object, I decided we should have a QuoteFinder object that would do all that lookup for us. The test (in Ruby) would look something like this:
quote_finder = flexmock(”quote finder“)
quote = flexmock(”quote“)
during {
… some function …
}.behold! {
quote_finder.should_receive(:find_quote).once.
with(…whatever…).
and_return(quote)
…
}
Next, the new quote had to be generated. The lazy way to do that would be to add that behavior to Quote itself:
quote_finder = flexmock(”quote finder“)
quote = flexmock(”quote“)
during {
… some function …
}.behold! {
quote_finder.should_receive(:find_quote).once.
with(…whatever…).
and_return(quote)
quote.should_receive(:create_revised_quote).once.
with(…whatever…).
and_return(”a new quote“)
}
Finally, the result of the function-under-test should be the new quote:
quote_finder = flexmock(”quote finder“)
quote = flexmock(”quote“)
during {
… some function …
}.behold! {
quote_finder.should_receive(:find_quote).once.
with(…whatever…).
and_return(quote)
quote.should_receive(:create_revised_quote).once.
with(…whatever…).
and_return(”a new quote“)
}
assert { @result == “a new quote“ }
I felt a bit of a fraud, since I’d shoved a lot of important behavior into tests that would need to be written by someone else (including the original purpose of the test, making sure the next Quote was a different object than the last one.) The team, though, gave me more credit than I did. They’d had two Aha! moments. First, the idea of “finding a quote” was spread throughout the code, and it would be better localized in a QuoteFinder object. Second, they decided it really did make sense to have Quotes make new versions of themselves (rather than leave that responsibility somewhere else). So this test gave the team two paths they could take to improve their code.
In the beginning, the QuoteFinder
and Quote#create_revised_quote
would likely just delegate their work to the existing legacy code, but there were now two new organizational centers that could attract behavior. So this looks a lot like Strangling an App, but it avoids that trick’s potential “then a miracle occurs” problem of needing a good architecture to strangle with: instead, by following the make-an-object-when-you-hesitate strategy that mocking encourages, you can grow one.
I’ve not seen any writeup on using mocks to deal with legacy code. Have you?
P.S. It’s possible I’ve gotten details of the story wrong, but I think the essentials are correct.