1. Trang chủ
  2. » Công Nghệ Thông Tin

xunit test patterns refactoring test code phần 8 docx

95 237 0

Đ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

Thông tin cơ bản

Định dạng
Số trang 95
Dung lượng 1,04 MB

Nội dung

603 help prevent Obscure Tests by defi ning a Higher Level Language (see page 41) for defi ning tests. It is also helpful to keep the Test Utility Methods relatively small and self-contained. We can achieve this goal by passing all arguments to these methods explicitly as parameters (rather than using instance variables) and by returning any objects that the tests will require as explicit return values or updated parameters. To ensure that the Test Utility Methods have Intent-Revealing Names, we should let the tests pull the Test Utility Methods into existence rather than just inventing Test Utility Methods that we think may be needed later. This “out- side-in” approach to writing code avoids “borrowing tomorrow’s trouble” and helps us fi nd the minimal solution. Writing the reusable Test Utility Method is relatively straightforward. The trickier question is where we would put this method. If the Test Utility Method is needed only in Test Methods in a single Testcase Class (page 373), then we can put it onto that class. If we need the Test Utility Method in several classes, however, the solution becomes a bit more complicated. The key issue relates to type visibility. The client classes need to be able to see the Test Utility Method, and the Test Utility Method needs to be able to see all the types and classes on which it depends. When it doesn’t depend on many types/classes or when everything it depends on is visible from a single place, we can put the Test Utility Method into a common Testcase Superclass (page 638) that we defi ne for our project or company. If it depends on types/classes that cannot be seen from a single place that all the clients can see, then we may need to put the Test Utility Method on a Test Helper in the appropriate test package or subsystem. In larger systems with many groups of domain objects, it is common practice to have one Test Helper for each group (package) of related domain objects. Variation: Test Utility Test One major advantage of using Test Utility Methods is that otherwise Untestable Test Code (see Hard-to-Test Code on page 209) can now be tested with Self- Checking Tests. The exact nature of such tests varies based on the kind of Test Utility Method being tested but a good example is a Custom Assertion Test (see Custom Assertion). Motivating Example The following example shows a test as many novice test automaters would fi rst write it: public void testAddItemQuantity_severalQuantity_v1(){ Address billingAddress = null; Test Utility Method Test Utility Method Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 604 Chapter 24 Test Organization Patterns Address shippingAddress = null; Customer customer = null; Product product = null; Invoice invoice = null; try { // Fixture Setup billingAddress = new Address("1222 1st St SW", "Calgary", "Alberta", "T2N 2V2", "Canada"); shippingAddress = new Address("1333 1st St SW", "Calgary", "Alberta", "T2N 2V2", "Canada"); customer = new Customer( 99, "John", "Doe", new BigDecimal("30"), billingAddress, shippingAddress); product = new Product( 88, "SomeWidget", new BigDecimal("19.99")); invoice = new Invoice( customer ); // Exercise SUT invoice.addItemQuantity( product, 5 ); // Verify Outcome List lineItems = invoice.getLineItems(); if (lineItems.size() == 1) { LineItem actItem = (LineItem) lineItems.get(0); assertEquals("inv", invoice, actItem.getInv()); assertEquals("prod", product, actItem.getProd()); assertEquals("quant", 5, actItem.getQuantity()); assertEquals("discount", new BigDecimal("30"), actItem.getPercentDiscount()); assertEquals("unit price", new BigDecimal("19.99"), actItem.getUnitPrice()); assertEquals("extended", new BigDecimal("69.96"), actItem.getExtendedPrice()); } else { assertTrue("Invoice should have 1 item", false); } } finally { // Teardown deleteObject(invoice); deleteObject(product); deleteObject(customer); deleteObject(billingAddress); deleteObject(shippingAddress); } } This test is diffi cult to understand because it exhibits many code smells, includ- ing Obscure Test and Hard-Coded Test Data (see Obscure Test). Test Utility Method Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 605 Refactoring Notes We often create Test Utility Methods by mining existing tests for reusable logic when we are writing new tests. We can use an Extract Method [Fowler] refac- toring to pull the code for the Test Utility Method out of one Test Method and put it onto the Testcase Class as a Test Utility Method. From there, we may choose to move the Test Utility Method to a superclass by using a Pull Up Method [Fowler] refactoring or to another class by using a Move Method [Fowler] refactoring. Example: Test Utility Method Here’s the refactored version of the earlier test. Note how much simpler this test is to understand than the original version. And this is just one example of what we can achieve by using Test Utility Methods! public void testAddItemQuantity_severalQuantity_v13(){ final int QUANTITY = 5; final BigDecimal CUSTOMER_DISCOUNT = new BigDecimal("30"); // Fixture Setup Customer customer = findActiveCustomerWithDiscount(CUSTOMER_DISCOUNT); Product product = findCurrentProductWith3DigitPrice( ); Invoice invoice = createInvoice(customer); // Exercise SUT invoice.addItemQuantity(product, QUANTITY); // Verify Outcome final BigDecimal BASE_PRICE = product.getUnitPrice(). multiply(new BigDecimal(QUANTITY)); final BigDecimal EXTENDED_PRICE = BASE_PRICE.subtract(BASE_PRICE.multiply( CUSTOMER_DISCOUNT.movePointLeft(2))); LineItem expected = createLineItem( QUANTITY, CUSTOMER_DISCOUNT, EXTENDED_PRICE, product, invoice); assertContainsExactlyOneLineItem(invoice, expected); } Let’s go through the changes step by step. First, we replaced the code to create the Customer and the Product with calls to Finder Methods that retrieve those objects from an Immutable Shared Fixture. We altered the code in this way because we don’t plan to change these objects. protected Customer findActiveCustomerWithDiscount( BigDecimal percentDiscount) { return CustomerHome.findCustomerById( ACTIVE_CUSTOMER_WITH_30PC_DISCOUNT_ID); } Test Utility Method Test Utility Method Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 606 Chapter 24 Test Organization Patterns Next, we introduced a Creation Method for the Invoice to which we plan to add the LineItem. protected Invoice createInvoice(Customer customer) { Invoice newInvoice = new Invoice(customer); registerTestObject(newInvoice); return newInvoice; } List testObjects; protected void registerTestObject(Object testObject) { testObjects.add(testObject); } To avoid the need for In-line Teardown (page 509), we registered each of the objects we created with our Automated Teardown mechanism, which we call from the tearDown method. private void deleteTestObjects() { Iterator i = testObjects.iterator(); while (i.hasNext()) { try { deleteObject(i.next()); } catch (RuntimeException e) { // Nothing to do; we just want to make sure // we continue on to the next object in the list. } } } public void tearDown() { deleteTestObjects(); } Finally, we extracted a Custom Assertion to verify that the correct LineItem has been added to the Invoice. void assertContainsExactlyOneLineItem( Invoice invoice, LineItem expected) { List lineItems = invoice.getLineItems(); assertEquals("number of items", lineItems.size(), 1); LineItem actItem = (LineItem)lineItems.get(0); assertLineItemsEqual("",expected, actItem); } Test Utility Method Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 607 Parameterized Test How do we reduce Test Code Duplication when the same test logic appears in many tests? We pass the information needed to do fi xture setup and result verifi cation to a utility method that implements the entire test life cycle. Testing can be very repetitious not only because we must run the same test over and over again, but also because many of the tests differ only slightly from one another. For example, we might want to run essentially the same test with slightly different system inputs and verify that the actual output varies accord- ingly. Each of these tests would consist of the exact same steps. While having a large number of tests is an excellent way to ensure good code coverage, it is not so attractive from a test maintainability standpoint because any change made to the algorithm of one of the tests must be propagated to all similar tests. A Parameterized Test offers a way to reuse the same test logic in many Test Methods (page 348). Setup Exercise Verify Teardown Fixture SUT Test Method1 Test Method2 Test Method n Data Data Data Setup Exercise Verify Teardown Fixture SUT Test Method1 Test Method2 Test Method n Data Data Data Parame- terized Test Parameterized Test Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 608 Chapter 24 Test Organization Patterns How It Works The solution, of course, is to factor out the common logic into a utility method. When this logic includes all four parts of the entire Four-Phase Test (page 358) life cycle—that is, fi xture setup, exercise SUT, result verifi cation, and fi xture teardown—we call the resulting utility method a Parameterized Test. This kind of test gives us the best coverage with the least code to maintain and makes it very easy to add more tests as they are needed. If the right utility method is available to us, we can reduce a test that would otherwise require a series of complex steps to a single line of code. As we detect similarities between our tests, we can factor out the commonalities into a Test Utility Method (page 599) that takes only the information that differs from test to test as its arguments. The Test Methods pass in as parameters any information that the Parameterized Test requires to run and that varies from test to test. When to Use It We can use a Parameterized Test whenever Test Code Duplication (page 213) results from several tests implementing the same test algorithm but with slightly different data. The data that differs becomes the arguments passed to the Param- eterized Test, and the logic is encapsulated by the utility method. A Parameterized Test also helps us avoid Obscure Tests (page 186); by reducing the number of times the same logic is repeated, it can make the Testcase Class (page 373) much more compact. A Parameterized Test is also a good steppingstone to a Data- Driven Test (page 288); the name of the Parameterized Test maps to the verb or “action word” of the Data-Driven Test, and the parameters are the attributes. If our extracted utility method doesn’t do any fi xture setup, it is called a Verifi cation Method (see Custom Assertion on page 474). If it also doesn’t exercise the SUT, it is called a Custom Assertion. Implementation Notes We need to ensure that the Parameterized Test has an Intent-Revealing Name [SBPP] so that readers of the test will understand what it is doing. This name should imply that the test encompasses the whole life cycle to avoid any con- fusion. One convention is to start or end the name in “test”; the presence of parameters conveys the fact that the test is parameterized. Most members of the xUnit family that implement Test Discovery (page 393) will create only Testcase Objects (page 382) for “no arg” methods that start with “test,” so this restriction shouldn’t prevent us from starting our Parameterized Test names with “test.” At least one member of the xUnit family—MbUnit—implements Parame- terized Test Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 609 Parameterized Tests at the Test Automation Framework (page 298) level. Extensions are becoming available for other members of the xUnit family, with DDSteps for JUnit being one of the fi rst to appear. Testing zealots would advocate writing a Self-Checking Test (see page 26) to verify the Parameterized Test. The benefi ts of doing so are obvious—including increased confi dence in our tests—and in most cases it isn’t that hard to do. It is a bit harder than writing unit tests for a Custom Assertion because of the inter- action with the SUT. We will likely need to replace the SUT 2 with a Test Double so that we can observe how it is called and control what it returns. Variation: Tabular Test Several early reviewers of this book wrote to me about a variation of Param- eterized Test that they use regularly: the Tabular Test. The essence of this test is the same as that for a Parameterized Test, except that the entire table of values resides in a single Test Method. Unfortunately, this approach makes the test an Eager Test (see Assertion Roulette on page 224) because it verifi es many test conditions. This issue isn’t a problem when all of the tests pass, but it does lead to a lack of Defect Localization (see page 22) when one of the “rows” fails. Another potential problem is that “row tests” may depend on one another either on purpose or by accident because they are running on the same Testcase Object; see Incremental Tabular Test for an example of this behavior. Despite these potential issues, Tabular Tests can be a very effective way to test. At least one member of the xUnit family implements Tabular Tests at the framework level: MbUnit provides an attribute [RowTest] to indicate that a test is a Parameterized Test and another attribute [Row(x,y, )] to specify the parameters to be passed to it. Perhaps it will be ported to other members of the xUnit family? (Hint, hint!) Variation: Incremental Tabular Test An Incremental Tabular Test is a variant of the Tabular Test pattern in which we deliberately build on the fi xture left over by the previous rows of the test. It is identical to a deliberate form of Interacting Tests (see Erratic Test on page 228) 2 The terminology of SUT becomes very confusing in this case because we cannot replace the SUT with a Test Double if it truly is the SUT. Strictly speaking, we are replacing the object that would normally be the SUT with respect to this test. Because we are actually verifying the behavior of the Parameterized Test, whatever normally plays the role of SUT for this test now becomes a DOC. (My head is starting to hurt just describing this; fortunately, it really isn’t very complicated and will make a lot more sense when you actually try it out.) Parameterized Test Parame- terized Test Also known as: Row Test Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 610 Chapter 24 Test Organization Patterns called Chained Tests (page 454), except that all the tests reside within the same Test Method. The steps within the Test Method act somewhat like the steps of a “DoFixture” in Fit but without individual reporting of failed steps. 3 Variation: Loop-Driven Test When we want to test the SUT with all the values in a particular list or range, we can call the Parameterized Test from within a loop that iterates over the values in the list or range. By nesting loops within loops, we can verify the behavior of the SUT with combinations of input values. The main requirement for doing this type of testing is that we must either enumerate the expected result for each input value (or combination) or use a Calculated Value (see Derived Value on page 718) without introducing Production Logic in Test (see Conditional Test Logic on page 200). A Loop-Driven Test suffers from many of the same issues associated with a Tabular Test, however, because we are hiding many tests inside a single Test Method (and, therefore, Testcase Object). Motivating Example The following example includes some of the runit (Ruby Unit) tests from the Web site publishing infrastructure I built in Ruby while writing this book. All of the Simple Success Tests (see Test Method) for my cross-referencing tags went through the same sequence of steps: defi ning the input XML, defi ning the expected HTML, stubbing out the output fi le, setting up the handler for the XML, extracting the resulting HTML, and comparing it with the expected HTML. def test_extref # setup sourceXml = "<extref id='abc'/>" expectedHtml = "<a href='abc.html'>abc</a>" mockFile = MockFile.new @handler = setupHandler(sourceXml, mockFile) # execute @handler.printBodyContents # verify assert_equals_html( expectedHtml, mockFile.output, "extref: html output") end def testTestterm_normal sourceXml = "<testterm id='abc'/>" expectedHtml = "<a href='abc.html'>abc</a>" 3 This is because most members of the xUnit terminate the Test Method on the fi rst failed assertion. Parame- terized Test Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 611 mockFile = MockFile.new @handler = setupHandler(sourceXml, mockFile) @handler.printBodyContents assert_equals_html( expectedHtml, mockFile.output, "testterm: html output") end def testTestterm_plural sourceXml ="<testterms id='abc'/>" expectedHtml = "<a href='abc.html'>abcs</a>" mockFile = MockFile.new @handler = setupHandler(sourceXml, mockFile) @handler.printBodyContents assert_equals_html( expectedHtml, mockFile.output, "testterms: html output") end Even though we have already factored out much of the common logic into the setupHandler method, some Test Code Duplication remains. In my case, I had at least 20 tests that followed this same pattern (with lots more on the way), so I felt it was worthwhile to make these tests really easy to write. Refactoring Notes Refactoring to a Parameterized Test is a lot like refactoring to a Custom Asser- tion. The main difference is that we include the calls to the SUT made as part of the exercise SUT phase of the test within the code to which we apply the Extract Method [Fowler] refactoring. Because these tests are virtually identical once we have defi ned our fi xture and expected results, the rest can be extracted into the Parameterized Test. Example: Parameterized Test In the following tests, we have reduced each test to two steps: initializing two variables and calling a utility method that does all the real work. This utility method is a Parameterized Test. def test_extref sourceXml = "<extref id='abc' />" expectedHtml = "<a href='abc.html'>abc</a>" generateAndVerifyHtml(sourceXml,expectedHtml,"<extref>") end def test_testterm_normal sourceXml = "<testterm id='abc'/>" expectedHtml = "<a href='abc.html'>abc</a>" generateAndVerifyHtml(sourceXml,expectedHtml,"<testterm>") Parameterized Test Parame- terized Test Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 612 Chapter 24 Test Organization Patterns end def test_testterm_plural sourceXml = "<testterms id='abc'/>" expectedHtml = "<a href='abc.html'>abcs</a>" generateAndVerifyHtml(sourceXml,expectedHtml,"<plural>") end The succinctness of these tests is made possible by defi ning the Parameterized Test as follows: def generateAndVerifyHtml( sourceXml, expectedHtml, message, &block) mockFile = MockFile.new sourceXml.delete!("\t") @handler = setupHandler(sourceXml, mockFile ) block.call unless block == nil @handler.printBodyContents actual_html = mockFile.output assert_equal_html( expectedHtml, actual_html, message + "html output") actual_html end What distinguishes this Parameterized Test from a Verifi cation Method is that it contains the fi rst three phases of the Four-Phase Test (from setup to verify), whereas the Verifi cation Method performs only the exercise SUT and verify re- sult phases. Note that our tests did not need the teardown phase because we are using Garbage-Collected Teardown (page 500). Example: Independent Tabular Test Here’s an example of the same tests coded as a single Independent Tabular Test: def test_a_href_Generation row( "extref" ,"abc","abc.html","abc" ) row( "testterm" ,'abc',"abc.html","abc" ) row( "testterms",'abc',"abc.html","abcs") end def row( tag, id, expected_href_id, expected_a_contents) sourceXml = "<" + tag + " id='" + id + "'/>" expectedHtml = "<a href='" + expected_href_id + "'>" + expected_a_contents + "</a>" msg = "<" + tag + "> " generateAndVerifyHtml( sourceXml, expectedHtml, msg) end Parame- terized Test Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com [...]... http://www.simpopdf.com Testcase Superclass Where do we put our test code when it is in reusable Test Utility Methods? Also known as: Abstract Testcase, Abstract Test Fixture, Testcase Baseclass We inherit reusable test- specific logic from an abstract Testcase Super class Testcase Superclass Test Utility Method_1 Test Utility Method_2 Fixture SUT testMethod_1 testMethod_n Testcase Superclass Testcase Class As we write tests,... organize our Test Methods onto Testcase Classes? We organize Test Methods into Testcase Classes based on commonality of the test fixture FixtureATestcaseClass FixtureATestcaseClass setUp Creation Fixture A testMethod_1 testMethod_2 Exercise feature_1 SUT Class FixtureBTestcaseClass Exercise feature_2 setUp testMethod_1 testMethod_2 Fixture B Creation Testcase Class per Fixture As the number of Test Methods... http://www.simpopdf.com Testcase Class per Class How do we organize our Test Methods onto Testcase Classes? We put all the Test Methods for one SUT class onto a single Testcase Class TestcaseClass Creation Fixture A testMethod_A_1 testMethod_A_2 Exercise feature_1 SUT Class Exercise feature_2 testMethod_B_1 testMethod_B_2 Creation Fixture B As the number of Test Methods (page 3 48) grows, we need to decide on which Testcase... in the Test Method names Testcase Class per Class 624 Chapter 24 Test Organization Patterns Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Testcase Class per Feature How do we organize our Test Methods onto Testcase Classes? We group the Test Methods onto Testcase Classes based on which testable feature of the SUT they exercise Feature1TestcaseClass Creation Fixture A testMethod_A... keeping track of the Test Methods is becoming a bit of a chore Because the Test Methods on this Testcase Class require three distinct test fixtures (one for each state the flight can be in), it is a good example of a test that can be improved through refactoring to Testcase Class per Fixture Refactoring Notes We can remove Test Code Duplication (page 213) in the fixture setup and make the Test Methods easier... testMethod_A Outputs) testMethod_B Exercise feature_1 SUT Class Feature2TestcaseClass Exercise feature_2 testMethod_A testMethod_B Creation Fixture B Testcase Class per Feature As the number of Test Methods (page 3 48) grows, we need to decide on which Testcase Class (page 373) to put each Test Method Our choice of a test organization strategy affects how easily we can get a “big picture” view of our tests It also... track of the Test Methods is becoming a bit of a chore Because the Test Methods on this Testcase Class require four distinct methods, it is a good example of a test that can be improved through refactoring to Testcase Class per Feature Refactoring Notes We can reduce the size of each Testcase Class and make the names of the Test Methods more meaningful by converting them to follow the Testcase Class... to run all the tests for this class, we should put these Testcase Classes into a single nested folder, package, or namespace We can use an AllTests Suite (see Named Test Suite on page 592) to aggregate all of the Testcase Classes into a single test suite if we are using Test Enumeration (page 399) Motivating Example This example uses the Testcase Class per Class pattern to structure the Test Methods... put each Test Method Our choice of a test organization strategy affects how easily we can get a “big picture” view of our tests It also affects our choice of a fixture setup strategy Using a Testcase Class per Class is a simple way to start off organizing our tests How It Works We create a separate Testcase Class for each class we wish to test Each Testcase Class acts as a home to all the Test Methods... SUT class Testcase Class per Class 6 18 Chapter 24 Test Organization Patterns Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com When to Use It Using a Testcase Class per Class is a good starting point when we don’t have very many Test Methods or we are just starting to write tests for our SUT As the number of tests increases and we gain a better understanding of our test fixture . Variation: Test Utility Test One major advantage of using Test Utility Methods is that otherwise Untestable Test Code (see Hard-to -Test Code on page 209) can now be tested with Self- Checking Tests Calculator(); TestValues[] testValues = { new TestValues(1,2,3), new TestValues(2,3,5), new TestValues(3,4 ,8) , // special case! new TestValues(4,5,9) }; for (int i = 0; i < testValues.length;. the tests must be propagated to all similar tests. A Parameterized Test offers a way to reuse the same test logic in many Test Methods (page 3 48) . Setup Exercise Verify Teardown Fixture SUT Test Method1 Test Method2 Test Method

Ngày đăng: 14/08/2014, 01:20

TỪ KHÓA LIÊN QUAN