Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 95 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
95
Dung lượng
679,66 KB
Nội dung
698 Chapter 26 Design-for-Testability Patterns the transaction. As a consequence, we can exercise the logic, verify the outcome, and then abort the transaction, leaving no trace of our activity in the database. To implement Humble Transaction Controller, we use an Extract Method [Fowler] refactoring to move all the logic we want to test out of the code that controls the transaction and into a separate method that knows nothing about transaction control and that can be called by the test. Because the caller con- trols the transaction, the test can start, commit (if it so chooses), and (most commonly) roll back the transaction. In this case, the behavior—not the dependencies—causes us to bypass the Humble Object when we are testing the business logic. As a result, we are more likely to be able to get away with a Poor Man’s Humble Object. As for the Humble Object, it contains no business logic. Thus the only behavior that needs to be tested is whether the Humble Object commits and rolls back the transaction properly based on the outcome of the methods it calls. We can write a test that replaces the testable component with a Test Stub (page 529) that throws an exception and then verify that this activity results in a rollback of the transac- tion. If we are using a Poor Man’s Humble Object, the stub would be implemented as a Subclassed Test Double (see Test-Specifi c Subclass on page 579) that overrides the “real” methods with methods that throw exceptions. Many of the major application server technologies support this pattern either directly or indirectly by taking transaction control away from the business objects that we write. If we are building our software without using a transaction control framework, we may need to implement our own Humble Transaction Controller. See the “Implementation Notes” section for some ideas on how we can enforce the separation. Variation: Humble Container Adapter Speaking of “containers,” we often have to implement specifi c interfaces to allow our objects to run inside an application server (e.g., the “EJB session bean” interface). Another variation on the Humble Object pattern is to design our objects to be container-independent and then have a Humble Container Adapter adapt them to the interface required by container. This strategy makes our logic components easy to test outside the container, which dramatically reduces the time required for an “edit–compile–test” cycle. Implementation Notes We can make the logic that normally runs inside the Humble Object testable in several different ways. All of these techniques share one commonality: They in- volve exposing the logic so that it can be verifi ed using synchronous tests. They Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 699 vary, however, in terms of how the logic is exposed. Regardless of how logic ex- posure occurs, test-driven purists would prefer that tests verify that the Humble Object is calling the extracted logic properly. This can be done by replacing the real logic methods with some kind of Test Double (page 522) implementation. Variation: Poor Man’s Humble Object The simplest way to isolate and expose each piece of logic we want to verify is to place it into a separate method. We can do so by using an Extract Method refactoring on in-line logic and then making the resulting method visible from the test. Of course, this method cannot require anything from the context. Ideally everything the method needs to do its work will be passed in as arguments but this information could also be placed in fi elds. Problems may arise if the testable com- ponent needs to call methods to access information it needs and those methods are dependent on the (nonexistent/faked) context, as this dependency makes writing the tests more complex. This approach, which constitutes the “poor man’s” Humble Object, works well if no obstacles prevent the instantiation of the Humble Object (e.g., automatically starting its thread, no public constructor, unsatisfi able dependencies). Use of a Test- Specifi c Subclass can also help break these dependencies by providing a test-friendly constructor and exposing private methods to the test. When testing a Subclassed Humble Object or a Poor Man’s Humble Object, we can build the Test Spy (page 538) as a Subclassed Test Double of the Humble Object to record when the methods in question were called. We can then use assertions within the Test Method (page 348) to verify that the values recorded match the values expected. Variation: True Humble Object At the other extreme, we can put the logic we want to test into a separate class and have the Humble Object delegate to an instance of it. This approach, which was implied in the introduction to this pattern, will work in almost any circum- stance where we have complete control over the code. Sometimes the host framework requires that its objects hold certain responsi- bilities that we cannot move elsewhere. For example, a GUI framework expects its view objects to contain data for the controls of the GUI and the data that those controls display on the screen. In these cases we must either give the test- able object a reference to the Humble Object and have it manipulate the data for that object or put some minimal update logic in the Humble Object and accept that it won’t be covered by automated tests. The former approach is almost always possible and is always preferable. Humble Object Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 700 Chapter 26 Design-for-Testability Patterns To refactor to a True Humble Object, we normally do a series of Extract Method refactorings to decouple the public interface of the Humble Object from the implementation logic we plan to delegate. Then we do an Extract Class [Fowler] refactoring to move all the methods—except the ones that defi ne the public interface of the Humble Object—to the new “testable” class. We introduce an attribute (a fi eld) to hold a reference to an instance of the new class and initial- ize it to an instance of the new class either as part of the constructor or using Lazy Initialization [SBPP] in each interface method. When testing a True Humble Object (where the Humble Object delegates to a separate class), we typically use a Lazy Mock Object (see Mock Object on page 544) or Test Spy to verify that the extracted class is called correctly. By contrast, using the more common Active Mock Object (see Mock Object) is problematic in this situation because the assertions are made on a different thread from the Testcase Object (page 382) and failures won’t be detected unless we fi nd a way to channel them back to the test thread. To ensure that the extracted testable component is instantiated properly, we can use an observable Object Factory (see Dependency Lookup on page 686) to construct the extracted component. The test can register as a listener to verify the correct method is called on the factory. We can also use a regular factory object and replace it during the test with a Mock Object or Test Stub to monitor which factory method was called. Variation: Subclassed Humble Object In between the extremes of the Poor Man’s Humble Object and the True Humble Object are approaches that involve clever use of subclassing to put the logic into separate classes while still allowing them to be on a single object. A number of different ways to do this are possible, depending on whether the Humble Object class needs to subclass a specifi c framework class. I won’t go into a lot of detail here as this technique is very specifi c to the language and runtime environment. Nevertheless, you should recognize that the basic options are either having the framework-dependent class inherit the logic to be tested from a superclass or having the class delegate to an abstract method that is implemented by a subclass. Motivating Example (Humble Executable) In this example, we are testing some logic that runs in its own thread and processes each request as it arrives. In each test, we start up the thread, send it some messages, and wait long enough so that our assertions pass. Unfortu- nately, it takes several seconds for the thread to start up, become initialized, Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 701 and process the fi rst request. Thus the test fails sporadically unless we include a two-second delay after starting the thread. public class RequestHandlerThreadTest extends TestCase { private static final int TWO_SECONDS = 3000; public void testWasInitialized_Async() throws InterruptedException { // Setup RequestHandlerThread sut = new RequestHandlerThread(); // Exercise sut.start(); // Verify Thread.sleep(TWO_SECONDS); assertTrue(sut.initializedSuccessfully()); } public void testHandleOneRequest_Async() throws InterruptedException { // Setup RequestHandlerThread sut = new RequestHandlerThread(); sut.start(); // Exercise enqueRequest(makeSimpleRequest()); // Verify Thread.sleep(TWO_SECONDS); assertEquals(1, sut.getNumberOfRequestsCompleted()); assertResponseEquals(makeSimpleResponse(), getResponse()); } } Ideally, we would like to test the thread with each kind of transaction individu- ally to achieve better Defect Localization (see page 22). Unfortunately, if we did so our test suite would take many minutes to run because each test includes a delay of several seconds. Another problem is that the tests won’t result in an error if our active object has an exception in its own thread. A two-second delay may not seem like a big deal, but consider what happens when we have a dozen such tests. It would take us almost half a minute to run these tests. Contrast this performance with that of normal tests—we can run several hundred of those tests each second. Testing via the executable is affecting our productivity negatively. For the record, here’s the code for the executable: public class RequestHandlerThread extends Thread { private boolean _initializationCompleted = false; private int _numberOfRequests = 0; public void run() { initializeThread(); processRequestsForever(); } Humble Object Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 702 Chapter 26 Design-for-Testability Patterns public boolean initializedSuccessfully() { return _initializationCompleted; } void processRequestsForever() { Request request = nextMessage(); do { Response response = processOneRequest(request); if (response != null) { putMsgOntoOutputQueue(response); } request = nextMessage(); } while (request != null); } } To avoid the distraction of the business logic, I have already used an Extract Method refactoring to move the real logic into the method processOneRequest. Likewise, the actual initialization logic is not shown here; suffi ce it to say that this logic sets the variable _initializationCompleted when it fi nishes successfully. Refactoring Notes To create a Poor Man’s Humble Object, we expose the methods to make them visible from the test. (If the code used in-line logic, we would do an Extract Method refactoring fi rst.) If there were any dependencies on the context, we would need to do an Introduce Parameter [JBrains] refactoring or an Introduce Field [JetBrains] refactoring so that the processOneRequest method need not access anything from the context. To create a true Humble Object, we can do an Extract Class refactoring on the executable to create the testable component, leaving behind just the Humble Object as an empty shell. This step typically involves doing the Extract Method refactoring described above to separate the logic we want to test (e.g., the initializeThread method and the processOneRequest method) from the logic that interacts with the context of the executable. We then do an Extract Class refactoring to introduce the testable component class (essentially a single Strategy [GOF] object) and move all methods except the public interface methods over to it. The Extract Class refac- toring includes introducing a fi eld to hold a reference to the new object and creating an instance. It also includes fi xing all of the public methods so that they call the methods that were moved to the new testable class. Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 703 Example: Poor Man’s Humble Executable Here is the same set of tests rewritten as a Poor Man’s Humble Object: public void testWasInitialized_Sync() throws InterruptedException { // Setup RequestHandlerThread sut = new RequestHandlerThread(); // Exercise sut.initializeThread(); // Verify assertTrue(sut.initializedSuccessfully()); } public void testHandleOneRequest_Sync() throws InterruptedException { // Setup RequestHandlerThread sut = new RequestHandlerThread(); // Exercise Response response = sut.processOneRequest(makeSimpleRequest()); // Verify assertEquals(1, sut.getNumberOfRequestsCompleted()); assertResponseEquals(makeSimpleResponse(), response); } Here, we have made the methods initializeThread and processOneRequest public so that we can call them synchronously from the test. Note the absence of a delay in this test. This approach works well as long as we can instantiate the executable component easily. Example: True Humble Executable Here is the code for our SUT refactored to use a True Humble Executable: public class HumbleRequestHandlerThread extends Thread implements Runnable { public RequestHandler requestHandler; public HumbleRequestHandlerThread() { super(); requestHandler = new RequestHandlerImpl(); } public void run() { requestHandler.initializeThread(); processRequestsForever(); } public boolean initializedSuccessfully() { Humble Object Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 704 Chapter 26 Design-for-Testability Patterns return requestHandler.initializedSuccessfully(); } public void processRequestsForever() { Request request = nextMessage(); do { Response response = requestHandler.processOneRequest(request); if (response != null) { putMsgOntoOutputQueue(response); } request = nextMessage(); } while (request != null); } Here, we have moved the method processOneRequest to a separate class that we can instantiate easily. Below is the same test rewritten to take advantage of the extracted component. Note the absence of a delay in this test. public void testNotInitialized_Sync() throws InterruptedException { // Setup/Exercise RequestHandler sut = new RequestHandlerImpl(); // Verify assertFalse("init", sut.initializedSuccessfully()); } public void testWasInitialized_Sync() throws InterruptedException { // Setup RequestHandler sut = new RequestHandlerImpl(); // Exercise sut.initializeThread(); // Verify assertTrue("init", sut.initializedSuccessfully()); } public void testHandleOneRequest_Sync() throws InterruptedException { // Setup RequestHandler sut = new RequestHandlerImpl(); // Exercise Response response = sut.processOneRequest( makeSimpleRequest() ); // Verify assertEquals( 1, sut.getNumberOfRequestsDone()); assertResponseEquals( makeSimpleResponse(), response); } Because we have introduced delegation to another object, we should probably verify that the delegation occurs properly. The next test verifi es that the Humble Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 705 Object calls the initializeThread method and the processOneRequest method on the newly created testable component: public void testLogicCalled_Sync() throws InterruptedException { // Setup RequestHandlerRecordingStub mockHandler = new RequestHandlerRecordingStub(); HumbleRequestHandlerThread sut = new HumbleRequestHandlerThread(); // Mock Installation sut.setHandler( mockHandler ); sut.start(); // Exercise enqueRequest(makeSimpleRequest()); // Verify Thread.sleep(TWO_SECONDS); assertTrue("init", mockHandler.initializedSuccessfully() ); assertEquals( 1, mockHandler.getNumberOfRequestsDone() ); } Note that this test does require at least a small delay to allow the thread to start up. The delay is shorter, however, because we have replaced the real logic component with a Test Double that responds instantly and only one test now requires the delay. We could even move this test to a separate test suite that is run less frequently (e.g., only during the automated build process) to ensure that all tests performed before each check-in run quickly. The other signifi cant thing to note is that we are using a Test Spy rather than a Mock Object. Because the assertions done by the Mock Object would be raised in a different thread from the Test Method, the Test Automation Frame- work (page 298)—in this example, JUnit—won’t catch them. As a consequence, the test might indicate “pass” even though assertions in the Mock Object are failing. By making the assertions in the Test Method, we avoid having to do something special to relay the exceptions thrown by the Mock Object back to the thread in which the Test Method is executing. The preceding test verifi ed that our Humble Object actually delegates to the Test Spy that we have installed. It would also be a good idea to verify that our Humble Object actually initializes the variable holding the delegate to the appropriate class. Here’s a simple way to do so: public void testConstructor() { // Exercise HumbleRequestHandlerThread sut = new HumbleRequestHandlerThread(); // Verify String actualDelegateClass = sut.requestHandler.getClass().getName(); assertEquals( RequestHandlerImpl.class.getName(), actualDelegateClass); } Humble Object Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 706 Chapter 26 Design-for-Testability Patterns This Constructor Test (see Test Method) verifi es that a specifi c attribute has been initialized. Example: Humble Dialog Many development environments let us build the user interface visually by dragging and dropping various objects (“widgets”) onto a canvas. They let us add behavior to these visual objects by selecting one of several possible actions or events specifi c to that visual object and typing logic into the code window presented by the IDE. This logic may involve invoking the application behind the user interface or it may involve modifying the state of this or some other visual object. Visual objects are very diffi cult to test effi ciently because they are tightly coupled to the presentation framework that invokes them. To provide the visual object with all the information and facilities it requires, the test would need to simulate that environment—quite a challenge. This makes testing very complicated, so much so that many development teams don’t bother testing the presentation logic at all. This lack of testing, not surprisingly, often leads to Production Bugs caused by untested code and Untested Requirements. To create the Humble Dialog, we extract all the logic from the view com- ponent into a nonvisual component that is testable via synchronous tests. If this component needs to update the view object’s (Humble Dialog’s) state, the Humble Dialog is passed in as an argument. When testing the nonvisual com- ponent, we typically replace the Humble Dialog with a Mock Object that is confi gured with the indirect input values and the expected behavior (indirect outputs). In GUI frameworks that require the Humble Dialog to register itself with the framework for each event it wishes to see, the nonvisual component can register itself instead of the Humble Dialog (as long as that doesn’t introduce unmanageable dependencies on the context). This fl exibility makes the Humble Dialog even simpler because the events go directly to the nonvisual component and require no delegation logic. The following code sample is taken from a VB view component (.ctl) that includes some nontrivial logic. It is part of a custom plug-in we built for Mercury Interactive’s TestDirector tool. ' Interface method, TestDirector will call this method ' to display the results. Public Sub ShowResultEx(TestSetKey As TdTestSetKey, _ TSTestKey As TdTestKey, _ ResultKey As TdResultKey) Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 707 Dim RpbFiles As OcsRpbFiles Set RpbFiles = getTestResultFileNames(ResultKey) ResultsFileName = RpbFiles.ActualResultFileName ShowFileInBrowser ResultsFileName End Sub Function getTestResultFileNames(ResultKey As Variant) As OcsRpbFiles On Error GoTo Error Dim Attachments As Collection Dim thisTest As Run Dim RpbFiles As New OcsRpbFiles Call EnsureConnectedToTd Set Attachments = testManager.GetAllAttachmentsOfRunTest(ResultKey) Call RpbFiles.LoadFromCollection(Attachments, "RunTest") Set getTestResultFileNames = RpbFiles Exit Function Error: ' do something End Function Ideally, we would like to test the logic. Unfortunately, we cannot construct the objects passed in as parameters because they don’t have public constructors. Passing in objects of some other type isn’t possible either, because the types of the function parameters are hard-coded to be specifi c concrete classes. We can do an Extract Testable Component (page 735) refactoring on the ex- ecutable to create the testable component, leaving behind just the Humble Dialog as an empty shell. This approach typically involves doing several Extract Method refactorings (already done in the original example to make the refactoring easier to understand), one for each chunk of logic that we want to move. We then do an Extract Class refactoring to create our new testable component class. The Extract Class refactoring may include both Move Method [Fowler] and Move Field [Fowler] refactorings to move the logic and the data it requires out of the Humble Dialog and into the new testable component. Here’s the same view converted to a Humble Dialog: ' Interface method, TestDirector will call this method ' to display the results. Public Sub ShowResultEx(TestSetKey As TdTestSetKey, _ TSTestKey As TdTestKey, _ ResultKey As TdResultKey) Dim RpbFiles As OcsRpbFiles Call EnsureImplExists Set RpbFiles = Implementation.getTestResultFileNames(ResultKey) Humble Object Humble Object Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com [...]... use a Test Hook because we have no other way to address the Untested Code (see Production Bugs on page 268) caused by a Hard-Coded Dependency (see Hard-to -Test Code on page 2 09) 710 Chapter 26 Design-for-Testability Patterns Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com A Test Hook may be the only way to introduce Test Double (page 522) behavior when we are programming in a... dynamic binding Test Hooks can be used as a transition strategy to bring legacy code under the testing umbrella We can introduce testability using the Test Hooks and then use those Tests as Safety Net (see page 24) while we refactor for even more testability At some point we should be able to discard the initial round of tests that required the Test Hooks because we have enough “modern” tests to protect... essence of the Test Hook pattern is that we insert some code into the SUT that lets us test it Regardless of how we insert this code into the SUT, the code itself can either • Divert control to a Test Double instead of the real object, or • Be the Test Double within the real object, or • Be a test- specific Decorator [GOF] that delegates to the real object when in production The flag that indicates testing is... included these values as hard-coded literal constants In the next example, we’ll use symbolic constants instead Refactoring Notes We can reduce the Test Code Duplication (page 213) in the form of the hardcoded Literal Value of 19. 95 by doing a Replace Magic Number with Symbolic Constant [Fowler] refactoring Example: Symbolic Constant This refactored version of the original test replaces the duplicated... Humble Dialog pattern Test Hook 7 09 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Test Hook How do we design the SUT so that we can replace its dependencies at runtime? We modify the SUT to behave differently during the test DOC SUT Setup Exercise Verify Exercise If Testing? Yes No Production Logic TestSpecific Logic If Testing? Usage No Yes Production Logic TestSpecific Logic... http://www.simpopdf.com Simpo PDF Merge andPatterns Literal Value How do we specify the values to be used in tests? Also known as: Hard-Coded Value, Constant Value We use literal constants for object attributes and assertions BigDecimal expectedTotal = new BigDecimal( "99 .95 "); The values we use for the attributes of objects in our test fixture and the expected outcome of our test are often related to one another... every piece of code depends on some other classes, objects, modules, or procedures To unit -test a piece of code properly, we would like to isolate it from its dependencies Such isolation is difficult to achieve if those dependencies are hard-coded within the code in the form of literal classnames Test Hook is a “method of last resort” for introducing test- specific behavior during automated testing How It... all the testing logic In languages that support preprocessors or compiler macros, such constructs may also be used to remove the Test Hook before the code enters the production phase The value of the flag can also be read in from configuration data or stored in a global variable that the test sets directly Motivating Example The following test cannot be made to pass “as is”: Test Hook public void testDisplayCurrentTime_AtMidnight()... SUT We then wrap the production code with an if/then/else control structure and put the test- specific logic into the then clause Example: Test Hook in System Under Test Here’s the production code modified to accommodate testing via a Test Hook: public String getCurrentTimeAsHtmlFragment() { Calendar theTime; try { if (TESTING) { theTime = new GregorianCalendar(); theTime.set(Calendar.HOUR_OF_DAY, 0); theTime.set(Calendar.MINUTE,... behavior and generating test conditions is to use different values each time we run the tests Using a Random Generated Value is one way to accomplish this goal While use of such values may seem like a good idea, it makes the tests nondeterministic (Nondeterministic Tests) and can make debugging failed tests very difficult Ideally, when a test fails, we want to be able to repeat that test failure on demand . situation, we use a Test Hook because we have no other way to address the Untested Code (see Production Bugs on page 268) caused by a Hard-Coded Dependency (see Hard-to -Test Code on page 2 09) . DOC SUT Usage Exercise If Testing? No Production Logic Test- Specific Logic Setup Exercise Verify Teardown If Testing? Yes No Production Logic Test- Specific Logic Yes DOC SUT Usage Exercise If Testing? No Production Logic Test- Specific Logic Setup Exercise Verify Teardown If Testing? Yes No Production Logic Test- Specific Logic Yes . dynamic binding. Test Hooks can be used as a transition strategy to bring legacy code under the testing umbrella. We can introduce testability using the Test Hooks and then use those Tests as Safety. the Test Hook pattern is that we insert some code into the SUT that lets us test it. Regardless of how we insert this code into the SUT, the code itself can either • Divert control to a Test