Tài liệu Growing Object-Oriented Software, Guided by Tests- P6 doc

50 294 1
Tài liệu Growing Object-Oriented Software, Guided by Tests- P6 doc

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

ptg development cycle is so critical—we always get into trouble when we don’t keep up that side of the bargain. Small Methods to Express Intent We have a habit of writing helper methods to wrap up small amounts of code—for two reasons. First, this reduces the amount of syntactic noise in the calling code that languages like Java force upon us. For example, when we disconnect the Sniper, the translatorFor() method means we don’t have to type "AuctionMessageTranslator" twice in the same line. Second, this gives a mean- ingful name to a structure that would not otherwise be obvious. For example, chatDisconnectorFor() describes what its anonymous class does and is less intrusive than defining a named inner class. Our aim is to do what we can to make each level of code as readable and self- explanatory as possible, repeating the process all the way down until we actually have to use a Java construct. Logging Is Also a Feature We defined XMPPFailureReporter to package up failure reporting for the AuctionMessageTranslator . Many teams would regard this as overdesign and just write the log message in place. We think this would weaken the design by mixing levels (message translation and logging) in the same code. We’ve seen many systems where logging has been added ad hoc by developers wherever they find a need. However, production logging is an external interface that should be driven by the requirements of those who will depend on it, not by the structure of the current implementation. We find that when we take the trouble to describe runtime reporting in the caller’s terms, as we did with the XMPPFailureReporter , we end up with more useful logs. We also find that we end up with the logging infrastructure clearly isolated, rather than scattered throughout the code, which makes it easier to work with. This topic is such a bugbear (for Steve at least) that we devote a whole section to it in Chapter 20. Chapter 19 Handling Failure 226 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg Part IV Sustainable Test-Driven Development This part discusses the qualities we look for in test code that keep the development “habitable.” We want to make sure the tests pull their weight by making them expressive, so that we can tell what’s important when we read them and when they fail, and by making sure they don’t become a maintenance drag themselves. We need to apply as much care and attention to the tests as we do to the production code, although the coding styles may differ. Difficulty in testing might imply that we need to change our test code, but often it’s a hint that our design ideas are wrong and that we ought to change the production code. We’ve written up these guidelines as separate chapters, but that has more to do with our need for a linear structure that will fit into a book. In practice, these qualities are all related to and support each other. Test-driven development combines testing, specification, and design into one holistic activity. 1 1. For us, a sign of this interrelatedness was the difficulty we had in breaking up the material into coherent chapters. From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg This page intentionally left blank From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg Chapter 20 Listening to the Tests You can see a lot just by observing. —Yogi Berra Introduction Sometimes we find it difficult to write a test for some functionality we want to add to our code. In our experience, this usually means that our design can be improved—perhaps the class is too tightly coupled to its environment or does not have clear responsibilities. When this happens, we first check whether it’s an opportunity to improve our code, before working around the design by making the test more complicated or using more sophisticated tools. We’ve found that the qualities that make an object easy to test also make our code responsive to change. The trick is to let our tests drive our design (that’s why it’s called test-driven development). TDD is about testing code, verifying its externally visible qualities such as functionality and performance. TDD is also about feedback on the code’s internal qualities: the coupling and cohesion of its classes, dependencies that are explicit or hidden, and effective information hiding—the qualities that keep the code maintainable. With practice, we’ve become more sensitive to the rough edges in our tests, so we can use them for rapid feedback about the design. Now when we find a feature that’s difficult to test, we don’t just ask ourselves how to test it, but also why is it difficult to test. In this chapter, we look at some common “test smells” that we’ve encountered and discuss what they might imply about the design of the code. There are two categories of test smell to consider. One is where the test itself is not well written—it may be unclear or brittle. Meszaros [Meszaros07] covers several such patterns in his “Test Smells” chapter. This chapter is concerned with the other category, where a test is highlighting that the target code is the problem. Meszaros has one pattern for this, called “Hard-to-Test Code.” We’ve picked out some common cases that we’ve seen that are relevant to our approach to TDD. 229 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg I Need to Mock an Object I Can’t Replace (without Magic) Singletons Are Dependencies One interpretation of reducing complexity in code is making commonly useful objects accessible through a global structure, usually implemented as a singleton. Any code that needs access to a feature can just refer to it by its global name instead of receiving it as an argument. Here’s a common example: Date now = new Date(); Under the covers, the constructor calls the singleton System and sets the new instance to the current time using System.currentTimeMillis() . This is a conve- nient technique, but it comes at a cost. Let’s say we want to write a test like this: @Test public void rejectsRequestsNotWithinTheSameDay() { receiver.acceptRequest(FIRST_REQUEST); // the next day assertFalse("too late now", receiver.acceptRequest(SECOND_REQUEST)); } The implementation looks like this: public boolean acceptRequest(Request request) { final Date now = new Date(); if (dateOfFirstRequest == null) { dateOfFirstRequest = now; } else if (firstDateIsDifferentFrom(now)) { return false; } // process the request return true; } where dateOfFirstRequest is a field and firstDateIsDifferentFrom() is a helper method that hides the unpleasantness of working with the Java date library. To test this timeout, we must either make the test wait overnight or do some- thing clever (perhaps with aspects or byte-code manipulation) to intercept the constructor and return suitable Date values for the test. This difficulty in testing is a hint that we should change the code. To make the test easier, we need to control how Date objects are created, so we introduce a Clock and pass it into the Receiver . If we stub Clock , the test might look like this: @Test public void rejectsRequestsNotWithinTheSameDay() { Receiver receiver = new Receiver(stubClock); stubClock.setNextDate(TODAY); receiver.acceptRequest(FIRST_REQUEST); stubClock.setNextDate(TOMORROW); assertFalse("too late now", receiver.acceptRequest(SECOND_REQUEST)); } Chapter 20 Listening to the Tests 230 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg and the implementation like this: public boolean acceptRequest(Request request) { final Date now = clock.now(); if (dateOfFirstRequest == null) { dateOfFirstRequest = now; } else if (firstDateIsDifferentFrom(now)) { return false; } // process the request return true; } Now we can test the Receiver without any special tricks. More importantly, however, we’ve made it obvious that Receiver is dependent on time—we can’t even create one without a Clock . Some argue that this is breaking encapsulation by exposing the internals of a Receiver —we should be able to just create an in- stance and not worry—but we’ve seen so many systems that are impossible to test because the developers did not isolate the concept of time. We want to know about this dependency, especially when the service is rolled out across the world, and New York and London start complaining about different results. From Procedures to Objects Having taken the trouble to introduce a Clock object, we start wondering if our code is missing a concept: date checking in terms of our domain. A Receiver doesn’t need to know all the details of a calendar system, such as time zones and locales; it just need to know if the date has changed for this application. There’s a clue in the fragment: firstDateIsDifferentFrom(now) which means that we’ve had to wrap up some date manipulation code in Receiver . It’s the wrong object; that kind of work should be done in Clock . We write the test again: @Test public void rejectsRequestsNotWithinTheSameDay() { Receiver receiver = new Receiver(clock); context.checking(new Expectations() {{ allowing(clock).now(); will(returnValue(NOW)); one(clock).dayHasChangedFrom(NOW); will(returnValue(false)); }}); receiver.acceptRequest(FIRST_REQUEST); assertFalse("too late now", receiver.acceptRequest(SECOND_REQUEST)); } The implementation looks like this: 231 I Need to Mock an Object I Can’t Replace (without Magic) From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg public boolean acceptRequest(Request request) { if (dateOfFirstRequest == null) { dateOfFirstRequest = clock.now(); } else if (clock.dayHasChangedFrom(dateOfFirstRequest)) { return false; } // process the request return true; } This version of Receiver is more focused: it doesn’t need to know how to dis- tinguish one date from another and it only needs to get a date to set the first value. The Clock interface defines exactly those date services Receiver needs from its environment. But we think we can push this further. Receiver only retains a date so that it can detect a change of day; perhaps we should delegate all the date functionality to another object which, for want of a better name, we’ll call a SameDayChecker . @Test public void rejectsRequestsOutsideAllowedPeriod() { Receiver receiver = new Receiver(sameDayChecker); context.checking(new Expectations() {{ allowing(sameDayChecker).hasExpired(); will(returnValue(false)); }}); assertFalse("too late now", receiver.acceptRequest(REQUEST)); } with an implementation like this: public boolean acceptRequest(Request request) { if (sameDayChecker.hasExpired()) { return false; } // process the request return true; } All the logic about dates has been separated out from Receiver , which can concentrate on processing the request. With two objects, we can make sure that each behavior (date checking and request processing) is unit-tested cleanly. Implicit Dependencies Are Still Dependencies We can hide a dependency from the caller of a component by using a global value to bypass encapsulation, but that doesn’t make the dependency go away—it just makes it inaccessible. For example, Steve once had to work with a Microsoft .Net library that couldn’t be loaded without installing ActiveDirectory—which wasn’t actually required for the features he wanted to use and which he couldn’t install on his machine anyway. The library developer was trying to be helpful and to make it “just work,” but the result was that Steve couldn’t get it to work at all. Chapter 20 Listening to the Tests 232 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg One goal of object orientation as a technique for structuring code is to make the boundaries of an object clearly visible. An object should only deal with values and instances that are either local—created and managed within its scope—or passed in explicitly, as we emphasized in “Context Independence” (page 54). In the example above, the act of making date checking testable forced us to make the Receiver ’s requirements more explicit and to think more clearly about the domain. Use the Same Techniques to Break Dependencies in Unit Tests as in Production Code There are several frameworks available that use techniques such as manipulating class loaders or bytecodes to allow unit tests to break dependencies without changing the target code. As a rule, these are advanced techniques that most developers would not use when writing production code. Sometimes these tools really are necessary, but developers should be aware that they come with a hidden cost. Unit-testing tools that let the programmer sidestep poor dependency management in the design waste a valuable source of feedback.When the developers eventually do need to address these design weaknesses to add some urgent feature, they will find it harder to do. The poor structure will have influenced other parts of the system that rely on it, and any understanding of the original intent will have evaporated. As with dirty pots and pans, it’s easier to get the grease off before it’s been baked in. Logging Is a Feature We have a more contentious example of working with objects that are hard to replace: logging. Take a look at these two lines of code: log.error("Lost touch with Reality after " + timeout + "seconds"); log.trace("Distance traveled in the wilderness: " + distance); These are two separate features that happen to share an implementation. Let us explain. • Support logging (errors and info) is part of the user interface of the appli- cation. These messages are intended to be tracked by support staff, as well as perhaps system administrators and operators, to diagnose a failure or monitor the progress of the running system. • Diagnostic logging (debug and trace) is infrastructure for programmers. These messages should not be turned on in production because they’re in- tended to help the programmers understand what’s going on inside the system they’re developing. 233 Logging Is a Feature From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg Given this distinction, we should consider using different techniques for these two type of logging. Support logging should be test-driven from somebody’s re- quirements, such as auditing or failure recovery. The tests will make sure we’ve thought about what each message is for and made sure it works. The tests will also protect us from breaking any tools and scripts that other people write to analyze these log messages. Diagnostic logging, on the other hand, is driven by the programmers’ need for fine-grained tracking of what’s happening in the sys- tem. It’s scaffolding—so it probably doesn’t need to be test-driven and the mes- sages might not need to be as consistent as those for support logs. After all, didn’t we just agree that these messages are not to be used in production? Notification Rather Than Logging To get back to the point of the chapter, writing unit tests against static global objects, including loggers, is clumsy. We have to either read from the file system or manage an extra appender object for testing; we have to remember to clean up afterwards so that tests don’t interfere with each other and set the right level on the right logger. The noise in the test reminds us that our code is working at two levels: our domain and the logging infrastructure. Here’s a common example of code with logging: Location location = tracker.getCurrentLocation(); for (Filter filter : filters) { filter.selectFor(location); if (logger.isInfoEnabled()) { logger.info("Filter " + filter.getName() + ", " + filter.getDate() + " selected for " + location.getName() + ", is current: " + tracker.isCurrent(location)); } } Notice the shift in vocabulary and style between the functional part of the loop and the (emphasized) logging part. The code is doing two things at once—something to do with locations and rendering support information—which breaks the single responsibility principle. Maybe we could do this instead: Location location = tracker.getCurrentLocation(); for (Filter filter : filters) { filter.selectFor(location); support.notifyFiltering(tracker, location, filter);} where the support object might be implemented by a logger, a message bus, pop-up windows, or whatever’s appropriate; this detail is not relevant to the code at this level. This code is also easier to test, as you saw in Chapter 19. We, not the logging framework, own the support object, so we can pass in a mock implementation at our convenience and keep it local to the test case. The other simplification is that now we’re testing for objects, rather than formatted contents of a string. Of Chapter 20 Listening to the Tests 234 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg course, we will still need to write an implementation of support and some focused integration tests to go with it. But That’s Crazy Talk… The idea of encapsulating support reporting sounds like over-design, but it’s worth thinking about for a moment. It means we’re writing code in terms of our intent (helping the support people) rather than implementation (logging), so it’s more expressive. All the support reporting is handled in a few known places, so it’s easier to be consistent about how things are reported and to encourage reuse. It can also help us structure and control our reporting in terms of the application domain, rather than in terms of Java packages. Finally, the act of writing a test for each report helps us avoid the “I don’t know what to do with this exception, so I’ll log it and carry on” syndrome, which leads to log bloat and production failures because we haven’t handled obscure error conditions. One objection we’ve heard is, “I can’t pass in a logger for testing because I’ve got logging all over my domain objects. I’d have to pass one around everywhere.” We think this is a test smell that is telling us that we haven’t clarified our design enough. Perhaps some of our support logging should really be diagnostic logging, or we’re logging more than we need because of something that we wrote when we hadn’t yet understood the behavior. Most likely, there’s still too much dupli- cation in our domain code and we haven’t yet found the “choke points” where most of the production logging should go. So what about diagnostic logging? Is it disposable scaffolding that should be taken down once the job is done, or essential infrastructure that should be tested and maintained? That depends on the system, but once we’ve made the distinction we have more freedom to think about using different techniques for support and diagnostic logging. We might even decide that in-line code is the wrong technique for diagnostic logging because it interferes with the readability of the production code that matters. Perhaps we could weave in some aspects instead (since that’s the canonical example of their use); perhaps not—but at least we’ve now clarified the choice. One final data point. One of us once worked on a system where so much content was written to the logs that they had to be deleted after a week to fit on the disks. This made maintenance very difficult as the relevant logs were usually gone by the time a bug was assigned to be fixed. If they’d logged nothing at all, the system would have run faster with no loss of useful information. Mocking Concrete Classes One approach to interaction testing is to mock concrete classes rather than inter- faces. The technique is to inherit from the class you want to mock and override the methods that will be called within the test, either manually or with any of 235 Mocking Concrete Classes From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. [...]... should act as documentation for the code There are tools and IDE plug-ins that unpack the “camel case” method names and link them to the class under test, such as the TestDox plug-in for the IntelliJ IDE; Figure 21.1 shows the automatic documentation for a KeyboardLayout class Figure 21.1 The TestDox IntelliJ plug-in Regularly Read Documentation Generated from Tests We find that such generated documentation... could get just by looking at the target class; they break the “Don’t Repeat Yourself” principle [Hunt99] We don’t need to know that TargetObject has a choose() method—we need to know what the object does in different situations, what the method is for A better alternative is to name tests in terms of the features that the target object provides We use a TestDox convention (invented by Chris Stevenson)... void spinUpDisk() { […] public void eject() { […] } It turns out that our MusicCentre only uses the starting and stopping methods on the CdPlayer; the rest are used by some other part of the system We would be overspecifying the MusicCentre by requiring it to talk to a CdPlayer; what it actually needs is a ScheduledDevice Robert Martin made the point (back in 1996) in his Interface Segregation Principle... focused test with less code We can name the builder after the features that are common, and the domain objects after their differences This technique works best if the objects differ by the same fields If the objects vary by different fields, each build() will pick up the changes from the previous uses For example, it’s not obvious in this code that orderWithGiftVoucher will carry the 10% discount as well... builders as arguments rather than their objects This will simplify the test code by removing the build() methods The result is easier to read because it emphasizes the important information—what is being built—rather than the mechanics of building it For example, this code builds an order with no postcode, but it’s dominated by the builder infrastructure: Order orderWithNoPostcode = new OrderBuilder()... AddressBuilder().withNoPostcode().build()) build()) build(); We can remove much of the noise by passing around builders: Order order = new OrderBuilder() fromCustomer( new CustomerBuilder() withAddress(new AddressBuilder().withNoPostcode()))) build(); Emphasizing the Domain Model with Factory Methods We can further reduce the noise in the test code by wrapping up the construction of the builders in factory methods: Order... messageProcessor = new MessageProcessor(new XmlMessageUnpacker(counterpartyFinder), auditor, new MessageDispatcher( locationFinder, domesticNotifier, importedNotifier)); Later we can reduce the syntax noise by extracting out the creation of the MessageDispatcher Confused Object Another diagnosis for a “bloated constructor” might be that the object itself is too large because it has too many responsibilities... turns out that track is the only dependency of a RacingCar; the hint is that it’s the only field that’s final The listener is a notification, and everything else is an adjustment; all of these can be modified by the user before or during the race Here’s a reworked constructor: public class RacingCar { private final Track track; private private private private private private DrivingStrategy driver = DriverTypes.borderlineAggressiveDriving();... common defaults; the user can configure them later through the user interface, and we can configure them in our unit tests We’ve initialized the listener to a null object, again this can be changed later by the object’s environment Too Many Expectations When a test has too many expectations, it’s hard to see what’s important and what’s really under test For example, here’s a test: @Test public void decidesCasesWhenFirstPartyIsReady()... organization.getAdjudicator() repeatedly without breaking any behavior adjudicator.findCase() might go either way, but it happens to be a lookup so it has no side effects We can make our intentions clearer by distinguishing between stubs, simulations of real behavior that help us get the test to pass, and expectations, assertions we want to make about how an object interacts with its neighbors There’s a . the design by mixing levels (message translation and logging) in the same code. We’ve seen many systems where logging has been added ad hoc by developers. is an external interface that should be driven by the requirements of those who will depend on it, not by the structure of the current implementation.

Ngày đăng: 24/12/2013, 06:17

Từ khóa liên quan

Tài liệu cùng người dùng

Tài liệu liên quan