Have you ever seen or even developed a long, complex setup method in JUnit? Maybe it was because the production code was violating the Law of Demeter. Here's another Robert Martin Craftsman style blog.
Apprentice: Journeyman, I'm having trouble writing the fixtures for my JUnit tests. They take too long. I spend about 10 times longer writing tests than the production code.
Journeyman: Really? Unit test expert Gerard Meszaros says that tests should take only 10% to 20% of development time.
Apprentice: Well, I'm experiencing the exact opposite.
Journeyman: Let me take a look.
Apprentice: Sure.
Author Note: This is a trivial example for this blog entry. I've seen some really obscure test setup methods. The example is based on the sequence diagram chapter in Martin Fowler's UML Distilled book.
public class OrderTest { private Order order; @Before public void setUp() throws Exception { Product product1 = new Product(11.00); OrderLine line1 = new OrderLine(2, product1); Product product2 = new Product(22.00); OrderLine line2 = new OrderLine(3, product2); OrderLine[] lines = new OrderLine[] {line1, line2}; order = new Order(Arrays.asList(lines)); } @Test public void priceShouldBe88() { assertEquals(88.00, order.getPrice()); } }Journeyman: Okay, can I see the Order.getPrice() method?
public double getPrice() { double price = 0.00; for (OrderLine orderLine : orderLines) { Product product = orderLine.getProduct(); double productPrice = product.getPrice(); int quantity = orderLine.getQuantity(); price += productPrice * quantity; } return price; }Journeyman: Ah, I see the issue. Have you ever heard of the Law of Demeter (LoD)?
Apprentice: Huh?
Journeyman: The LoD has a few different names and related principles, but basically it means that an object should only deal with and only with its collaborators.
Apprentice: Huh?
Journeyman: You see how Order iterates through its OrderLine objects?
Apprentice: Yeah.
Journeyman: Well, that's fine. But then you grab the Product and then grab its price. You've violated the LoD. Order should only know about OrderLines. You've got more of a procedural design here, where you reach down into all the objects in the graph, grabbing all the data you need, and then do your thing. A more object oriented design would be to distribute the work. Do a little bit of work and delegate the rest to collaborating objects.
Apprentice: Oh, I think I'm beginning to see.
Journeyman: You should go read up on Craig Larman's GRASP principles. He calls the LoD Don't Talk to Strangers.
Apprentice: Okay, will do.
Journeyman: Have you been following test driven development (TDD)?
Apprentice: Uh..., well..., not really. But I do write the tests afterwards.
Journeyman: Complex test fixtures jump right out at you when you're doing TDD. You realize something is wrong right away. In fact, many times your testing style changes into more of an interaction based style. But I'm getting ahead of myself. Let's refactor this together.
Apprentice: Okay, cool, pair programming.
Journeyman: Right. Remember, Order should only deal with OrderLines.
Now, the setUp() method looks like:
@Before public void setUp() throws Exception { OrderLine line1 = stubOrderLineGetPriceToReturn(22.00); OrderLine line2 = stubOrderLineGetPriceToReturn(66.00); OrderLine[] lines = new OrderLine[] {line1, line2}; order = new Order(Arrays.asList(lines)); }And the Order.getPrice() method looks like:
public double getPrice() { double price = 0.00; for (OrderLine orderLine : orderLines) { price += orderLine.getPrice(); } return price; }Apprentice: Okay, now I really see. Order and its test are simpler, but don't we have the same amount of work anyway? We had to write OrderLine.getPrice() and test that.
Journeyman: Yes. The work is distributed. But I prefer lots of simpler objects with simpler test specifications than more complex objects and tests.
Apprentice: What do you mean, more objects? We didn't create any new objects.
Journeyman: I meant generally speaking, not necessarily in this case. Like I said, do a little work and pass the buck. That might mean creating Data Clumps, aggregate objects for collections, objects that represent abstract data types, and so on, but again, I'm getting ahead of myself.
Apprentice: Should I always follow the LoD?
Journeyman: Well, there are of course consequences. Classes tend to have larger APIs because sometimes they need to wrap the functionality of collaborating objects. And you need to step through more objects to understand an algorithm as a whole because it's distributed. But most of the time, I follow LoD because dependencies are reduced.
Apprentice: All right, I'll give it a shot. Thanks.
No comments:
Post a Comment