You are viewing an old revision of this post, from October 25, 2010 @ 21:37:55. See below for differences between this version and the current revision.
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 my 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.
Post Revisions:
- October 26, 2010 @ 07:02:52 [Current Revision] by David Carlton
- October 25, 2010 @ 21:37:55 by David Carlton
Changes:
October 25, 2010 @ 21:37:55 | Current Revision | ||
---|---|---|---|
Content | |||
Deleted: One of the things I got out of <a href="http:// malvasiabianca.org/archives/ 2010/10/agile- open-california- 2010-day-1/">Agile Open California</a> 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 <a href="http:// www.bactrian.org/~carlton/ dbcdb/1351/"> <cite>Growing Object-Oriented Software</cite></a> introduced | Added: One of the things I got out of <a href="http:// malvasiabianca.org/archives/ 2010/10/agile- open-california- 2010-day-1/">Agile Open California</a> 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 <a href="http:// www.bactrian.org/~carlton/ dbcdb/1351/"> <cite>Growing Object-Oriented Software</cite></a> 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 <em>too</em> many weeks (days, hopefully, if my next project is the right sort of thing) to for that to turn into a time savings. | ||
Unchanged: 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. | Unchanged: 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. | ||
Unchanged: 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! | Unchanged: 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! | ||
Unchanged: 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 <a href="http:// www.jmock.org/ getting-started.html">jMock boilerplate</a> for instantiating an instance of that interface; and poof, my tests were compiling, without any database-dependent code involved! | Unchanged: 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 <a href="http:// www.jmock.org/ getting-started.html">jMock boilerplate</a> for instantiating an instance of that interface; and poof, my tests were compiling, without any database-dependent code involved! | ||
Unchanged: 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. | Unchanged: 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. | ||
Unchanged: 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. | Unchanged: 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. | ||
Unchanged: 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. | Unchanged: 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. | ||
Unchanged: 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.) | Unchanged: 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.) | ||
Unchanged: 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. | Unchanged: 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. | ||
Unchanged: 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 <cite>Growing Object-Oriented Software</cite> 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. | Unchanged: 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 <cite>Growing Object-Oriented Software</cite> 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. |
Note: Spaces may be added to comparison text to allow better line wrapping.
Nice post. “Discovery” articles for mock object frameworks are always intriguing, because you can almost see the conceptual shift gradually play out, paragraph by paragraph. I myself haven’t read the Freeman book that you mentioned, but I have undergone a similar shift to TDD in my own job and, frankly, I still wonder how I ever managed to scrape by in object-oriented development before that discovery occurred.
Even if the subject matter is a little “inside baseball” for gaming, I think it’s supplemental to some of your previous points about the potential for Agile game development.
As an outsider, it seems like many of the graphical elements in game development may not fall under TDD coverage, but it also seems like you could certainly put much of the underlying implementation of the mechanics and design under the microscope. Can object-oriented TDD concepts (like mock objects or even something like dependency injection) actually inform not only the implementation of the design, but the formulation of the design itself?
(As a side note/curiosity: if you’re working in JDK 1.5 or later, you may want to check out Mockito as a mock objects framework. It has many of the same capabilities as JMock/EasyMock, but offers slightly easier syntax, as well as some spying capabilities that aren’t in play for the other frameworks, IIRC.)
10/26/2010 @ 9:59 am
Glad you enjoyed it! Not sure how long you’ve been reading, but I used to actually blog about agile here as much or more than I blogged about games, though that hasn’t been the case for a few years. (And it’s reassuring to see that at least some of my game-focused readers don’t mind this sort of thing!)
I’m still struggling with where in the UI layer to start TDDing. And then there’s a whole layer of game behavior / balance issues that TDDing doesn’t have much to say about, either. (Which I don’t have to worry so much about in the game I’m currently working on, given its JavaScript RPG nature, but I imagine I will have to worry about such issues more in the future.) I’m still convinced that TDD is the way to go when programming, but there are definitely aspects of game programming in particular where I’d like to better understand other techniques for helping with the quality of my code.
Thanks for the Mockito recommendation, I’ll give it a look. And definitely read the Freeman and Pryce book – I really appreciate the fact that it assumes that you already practice TDD and goes in depth into one specific way that some people like doing it, instead of providing another TDD tutorial.
10/26/2010 @ 9:37 pm