|
Methodology
[
Aslak Hellesoy
]
01:51, Monday, 12 April 2004
The most important benefit from Continuous Integration tools is instant notification of build failures. DamageControl supports this of course, but it also supports some interesting historic reports. You can see an example here: DamageControl stats for PicoContainer This kind of information will tell you wether your build process is heading in the right direction (build times going up or down). Seeing this will make it harder to ignore problems related to build time. -And hopefully encourage the development team to do something about it before it becomes too big a hassle. Other similar historic reports that I'm planning to write are Especially a historical JIRA activity graph can be used to determine the responsiveness of a project: o Bug lifetime distribution Combine all these graphs in a single page, and you get a pretty good picture of the health and activity of a project, both historically and currently.
[
Aslak Hellesoy
]
03:09, Sunday, 25 January 2004
patrah says: "Pico Container ideas are not new." Unfortunately, (s)he doesnt't mention *what* of the ideas are not new. (There is just "the idea"). The only thing that the authors of PicoContainer might have claimed as newness is to have a *framework* that supports constructor based dependency injection. As far as I can tell, there is no prior art to that. And for the IoC stuff: We never claimed it to be our invention (there are heaps of pre-pico IoC frameworks of course); We just came up with an framework that makes DI (which is just a part of the broader pattern/principle/technique referred to as IoC) simpler than previous DI oriented frameworks/techniques. Please be more specific about what isn't new (because there is more to PicoContainer than just C-DI) and blog it where people can leave comments. Oh well, leave'em here then.
[
Aslak Hellesoy
]
18:34, Monday, 12 January 2004
Recently I have been working on a team where a misunderstood mocking practice is common. (It's really an antipattern). Before I delve into the details, let me recap some simple mock essentials. A mock is an object that acts as a "dummy" placeholder for a "real" object. Its class is generally an implementation of an interface or a subclass of some class, either generated dynamically or statically coded. For a detailed explanation of what Mocks are and what they are intended to do, see MockObjects and JMock. Right, the problem with the codebase developed by this team I'm referring to is that many of the "mocks" replace the wrong classes. The result of this is overly complicated tests, in addition to a false perception of what is being tested. Many of the "mocks" in this codebase are not really proper mocks, apart from having the word "Mock" in their name. So where and how are these misunderstood "mocks" used? They are being used as substitutes for the classes that are supposed to be tested(!) In short, the unit tests are testing the "mocks". The system consists of WebWork Actions that talk to Hibernate via DAOs (Data Access Objects). -A fairly common combination of technologies and patterns. Most actions are following this kind of style: public class CheeseAction extends ActionSupport { private String cheese; public void setCheese(String cheese) { this.cheese = cheese; } public String getCheese() { return cheese; } public String execute() { try { // CheeseDao is a concrete class. saveCheese(new CheeseDao()); return SUCCESS; } catch (Exception e) { return ERROR; } } protected void saveCheese(CheeseDao dao) { dao.saveCheese(cheese); } } The CheeseDao class is where the Hibernate stuff happens. Calling the CheeseDao.saveCheese method will hit the database. Hitting the database when testing an Action is very time consuming. The team therefore decided to mock out the hibernate DAOs in the tests. The tests that test the actions follow this pattern: public class CheeseActionTest extends TestCase { public void testAddCheese() { // Yikes, we're testing the "mock" MockCheeseAction action = new MockCheeseAction(); action.setCheese("stilton"); assertEquals(ActionSupport.SUCCESS, action.execute()); assertEquals("stilton", action.getSavedCheese()); } public class MockCheeseAction extends CheeseAction { private String savedCheese; protected void saveCheese(CheeseDao dao) { CheeseDao mockDao = new CheeseDao() { public void saveCheese(String cheese) { savedCheese = cheese; } }; super.saveCheese(mockDao); } public String getSavedCheese() { return savedCheese; } } } (The development team's primary motivation for using mocks seems to have been to reduce the test execution time. While this is a noble goal, it is not the only good reason to use mocks. Another good reason to use them - and TDD in general - is to let good designs emerge more easily. Codebases developed with TDD and Mocks generally become a lot more decoupled than others). It must have been when someone was thinking about how to have the actions use a different DAO (a mock DAO) that this mocking antipattern was invented. (If my assumption about how this "mocking" technique was conceived is correct, it is also likely that the tests were written after the actions themselves. At least at the time of the invention of the antipattern. If the test had been written before the action, the likelyhood of having the action so tightly coupled with the Hibernate DAO would have been reduced). There is something fundamentally wrong about the "mocking" approach used in the test above: 1) The test is not testing the *real* action, but a subclass that only exists in the test codebase. The team's established practice is to extend the action and override methods that use objects that we want to mock out. Then, a mock object is created for the DAO (this is actually more like a proper mock), and the superclass' method is called with that object instead. In this trivial example it may not look so bad. After all, the overridden saveCheese method in the "mock" calls the superclass' method, right? But in the real codebase the tests and overridden actions are often so complicated (with many overridings) that this easily gets out of control. Specifically, if the superclass is refactored, there is no guarantee that the test is still valid. The derived "mock" implementation within the test may not be overriding anything in the superclass anymore, and the test may fail to execute the methods originally intended. 2) The Action class is not tested through its public interface, which is how it will be accessed when assembled in the application. 3) The test becomes overly complicated because both the class under test AND its dependencies have to be "mocked". 4) The expectations that are supposed to verify that the action invokes the dao's methods are clunky, as they require additional fields and getters on the subclass "mock". Mocks are substitutes for real classes. It is quite common to misunderstand what classes they are supposed to be substituted with mocks. Mocks are supposed to replace the objects that the class under test depends on. You should never substitute the class you intend to test with a Mock. If you do, you're not testing the real thing. Here is what the code should have looked like: (Also note the new name of the test method. I'll cover that in a new blog entry about "Unit Test Intents"). public class CheeseAction extends ActionSupport { private final CheeseDao dao; private String cheese; public void setCheese(String cheese) { this.cheese = cheese; } public String getCheese() { return cheese; } // Yes, you can do this. See http://wiki.opensymphony.com/space/PicoContainer+Integration public CheeseAction(CheeseDao dao) { this.dao = dao; } public String execute() { try { dao.saveCheese(cheese); return SUCCESS; } catch(Exception e) { return ERROR; } } } And the test (which should ideally be written before the action): public class CheeseActionTest extends TestCase { public void testExecuteCallsSaveCheeseOnDaoAndSucceedsWhenNoDaoException() { Mock mockDao = new Mock(CheeseDao.class); mockDao.expect("saveCheese", C.eq("stilton")); CheeseAction action = new CheeseAction((CheeseDao)mockDao.proxy()); action.setCheese("stilton"); assertEquals(ActionSupport.SUCCESS, action.execute()); mockDao.verify(); } } There are several benefits doing Mocking like this (the proper way): 1) The tests get a lot shorter. 2) It's more readable. 3) We're testing the right thing. 4) TDD buys us nice decoupled designs for free. This action is now a PicoComponent, honouring constructor based dependency injection. However, in order to be able to do things properly like this, the dependencies to be mocked out (in this case, the CheeseDao) should ideally be interfaces. This can incur some percieved overhead in the codebase, as the developer will now have to maintain both a CheeseDao and a HibernateCheeseDao. But it's better than having to maintain a CheeseAction and a MockCheeseAction. Recent additions to MockObjects (and soon JMock?) allow dynamic mocking of concrete classes too, using CGLib. And last, but not least. If you write your tests first, using mocks, you will end up with well-designed, nicely decoupled code. Post-testing and post-mocking is actually quite evil. It tends to open up and cripple the classes even more than they were before.
[
Aslak Hellesoy
]
17:26, Thursday, 1 January 2004
Paul is tired of the "TDD hype". He as many others seem to have misunderstood what it's all about. (TDD is not a testing technique). He also seems to ignore the benefits of TDD. "You'll spend more time writing tests than code." While in some cases this is true, or close to true, it is not a bad thing. Here is why: 1) You end up with less (functional) code than a with a non-TDD approach. This is because with TDD you don't write code you "think you might need". You write exactly what makes the test pass. 2) You know when you're done. Knowing when you're done saves time. 3) You gain confidence for refactoring. Being able to refactor is essential in order to obtain a maintainable and well designed codebase. It is impossible (or very time consuming at best) to refactor if you don't have tests. This is because you can't know whether your refactorings will break something without tests. Having a well designed codebase saves time. So while there might be some percieved overhead in doing TDD, the design and maintainability of your codebase will improve drastically. You will catch bugs sooner rather than later. This will in the long run lead to SHORTER development time. Don't get me wrong - I don't do TDD because I want to save time. I just wanted to argue that TDD does not slow you down. "You'll spend ages writing hugely complicated test cases." The TDD practioneers stress that unit tests shall be simple. The test fixture should be simple and the assertions few. If your tests are simple, so will your code will be. Systems developed without TDD are always more complicated than the TDD ones. If your test is too complicated, you're not doing it right. "You'll spend ages bug fixing the test harnesses that sometimes are more complicated than the code they're testing". This is like saying that cooking is crazy because there are bad chefs. There are good and bad ways to do TDD. The good one is to keep the tests simple. So do that. TDD is not easy. It is very hard to do properly until you pair with someone who knows how to do it. -Even for seasoned developers (who often have a lot of pride to swollow and a lot to unlearn). Many people think they are doing TDD while they are not. There is more to it than writing the tests first. -Like always doing the simplest thing and continuosly refactor the code. Mocking is also an essential part of the whole TDD concept. |