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

Phát triển Javascript - part 50 doc

10 137 0

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

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 10
Dung lượng 2,43 MB

Nội dung

ptg 17.1 Improving Readability 463 • Keep names as short as possible without sacrificing clarity. • Group-related tests in separate test cases and indicate the relation in the test case name, thus avoiding the same prefix in a large number of tests. • Never state what code is expected to do using the word “and;” doing so indicates the test is not specific enough, i.e., it is likely trying to test more than one aspect of the target method. • Focus on the what and why, not the how. 17.1.1.2 Breaking Free of Technical Limitations All of the tests in Part III, Real-World Test-Driven Development in JavaScript, were written using libraries that consider any method whose name starts with “test” to be a test. This leaves room for adding other properties on the test case that are not run as tests. In the interest of using libraries without modification, we have rolled with this, ending up with a bunch of tests with names starting with “test should,” which is a bit of a smell. Because we can easily add helper functions in a closure surrounding the test case, there really is no need for the test case to reserve space for helper methods (i.e., function properties whose names do not start with the obligatory “test”). By considering any function-valued property a test, test cases could allow more flexibility in the naming of tests. Luckily, wrapping, e.g., JsTestDriver’s TestCase function to do just that is simple. Listing 17.1 shows an enhanced test case function. It works just like the original, only all functions except setUp and tearDown are considered tests. Listing 17.1 Enhancing JsTestDriver’s test case function function testCaseEnhanced(name, tests) { var testMethods = {}; var property; for (var testName in tests) { property = tests[testName]; if (typeof property == "function" && !/^(setUp|tearDown)$/.test(testName)) { testName = "test " + testName; } testMethods[testName] = property; } return TestCase(name, testMethods); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 464 Writing Good Unit Tests The function simplyloops all the properties of the test object, prepends function property identifiers with “test,” and delegates to the original TestCase. Listing 17.2, shows a test originally from Chapter 12, Abstracting Browser Differences: Ajax, using the enhanced test case. Listing 17.2 Using the enhanced test case to improve test name clarity testCaseEnhanced("RequestTest", { /* */ "should obtain an XMLHttpRequest object": function () { ajax.get("/url"); assert(ajax.create.called); } /* */ }); 17.1.2 Structure Tests in Setup, Exercise, and Verify Blocks White space can be used to underline the inherent setup/exercise/verify structure of tests. Listing 17.3, originally from Chapter 15, TDD and DOM Manipulation: The Chat Client, shows a test for the user form controller that expects the handle- Submit method to notify observers of the submitted user name. Notice how blank lines separate each of the setup/exercise/verify phases of the test. Listing 17.3 Formatting tests with blank lines to improve readability "test should notify observers of username": function () { var input = this.element.getElementsByTagName("input")[0]; input.value = "Bullrog"; this.controller.setModel({}); this.controller.setView(this.element); var observer = stubFn(); this.controller.observe("user", observer); this.controller.handleSubmit(this.event); assert(observer.called); assertEquals("Bullrog", observer.args[0]); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 17.1 Improving Readability 465 Physically separatingsetup, exercise,and verification makes it dead simple to see what setup is required and how to exercise the given behavior, as well as identifying the success criteria. 17.1.3 Use Higher-Level Abstractions to Keep Tests Simple Unit tests should always target a single behavior, nothing more. Usually this corre- lates with a single assertion per test, but some behaviors are more complex to verify, thus requiring more assertions. Whenever we find ourselves repeating the same set of two or three assertions, we should consider introducing higher-level abstractions to keep tests short and clear. 17.1.3.1 Custom Assertions: Behavior Verification Custom assertions are one way to abstract away compound verification. The most glaring example of this from Part III, Real-World Test-Driven Development in JavaScript, is the behavior verification of the stubs. Listing 17.4 shows a slightly modified test for the Comet client that expects the client’s observers to be notified when the dispatch method is called. Listing 17.4 Expecting observers to be notified "test dispatch should notify observers": function () { var client = Object.create(ajax.cometClient); client.observers = { notify: stubFn() }; client.dispatch({ someEvent: [{ id: 1234 }] }); var args = client.observers.notify.args; assert(client.observers.notify.called); assertEquals("someEvent", args[0]); assertEquals({ id: 1234 }, args[1]); } Using the Sinon stubbing library introduced in Chapter 16, Mocking and Stubbing, we can verify the test using Sinon’s higher-level assertCalledWith method instead, which makes the test more clearly state its intent, as seen in Listing 17.5. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 466 Writing Good Unit Tests Listing 17.5 Expecting observers to be notified "test dispatch should notify observers": sinon.test(function (stub) { var client = Object.create(ajax.cometClient); var observers = client.observers; stub(observers, "notify"); client.dispatch({ custom: [{ id:1234 }] }); assertCalledWith(observers.notify, "custom", { id:1234 }); }) 17.1.3.2 Domain Specific Test Helpers Another example of repeated patterns from Part III, Real-World Test-Driven Development in JavaScript, that could be simplified by a higher-level ab- straction is testing of event handlers. Given that the chat client uses the custom dom.addEventHandler method in conjunction with Function. prototype.bind to bind event handlers, we could extract the scaffolding needed to test this into something like Listing 17.6. Listing 17.6 Testing event handlers using a higher-level abstraction "test should handle submit event with bound handleSubmit": function () { expectMethodBoundAsEventHandler( this.controller, "handleSubmit", "submit", function () { this.controller.setView(this.element); }.bind(this) ); } This simple test replaces two original tests from the user form controller’s test case, and the imaginary helper method abstracts away some of the cruft related to stubbing the handler method and addEventHandler, as well as obtaining a reference to the handler function passed to it to verify that it is called with the object as this. When introducing domain and/or project specific test helpers such as this, we can also test them to make sure they work as expected, and then use them throughout the project, reducing the amount of scaffolding test code considerably. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 17.1 Improving Readability 467 17.1.4 Reduce Duplication, Not Clarity As with production code, we should actively remove duplication from tests to keep them apt for change. If we decide to change the way we create objects of a given type, it is preferable if that doesn’t force us to change the creation of an object in 30 tests, unless all those tests specifically target the object creation. However, there is a fine line to walk when reducing duplication in tests—if we do it too aggressively, we may end up removing important communication from a test. A good way to check if you have slimmed down a test too much is to extract it from its test case along with the name of the test case; is it still clear what behavior the test is describing? If it is not, e.g., because properties are not self-explanatory, or the state of the system is not clear, then we have taken away too much. Listing 17.7 shows a test from the chat client’s message-list controller. The test does not include code to create a controller instance, but still manages to clearly state that setting the view with setView causes the element set as view to have its class name set to “js-chat.” Listing 17.7 Reading a test in isolation TestCase("MessageListControllerSetViewTest", { /* */ "test should set class to js-chat": function () { this.controller.setView(this.element); assertClassName("js-chat", this.element); } }); Notice how this test also uses the assertClassName assertion, which can be considered a high-level assertion. To avoid repeating too much code throughout Part III, Real-World Test-Driven Development in JavaScript, I may have sinned against this guideline a few times. Listing 17.8 shows a test from the same test case that expects addMessage to create new DOM elements and append them to the view. Listing 17.8 Possibly too aggressively DRYed test "test should add dd element with message": function () { this.controller.addMessage({ user: "Theodore", message: "We are one" }); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 468 Writing Good Unit Tests var dds = this.element.getElementsByTagName("dd"); assertEquals(1, dds.length); assertEquals("We are one", dds[0].innerHTML); } Although this test clearly states what happens when the addMessage method is called, it may not be immediately clear that this.element is associated with the controller by having been set through setView. Making the situation worse, we did not write a test that describes the fact that without first calling setView with a DOM element, the addMessage method is not able to do anything useful—a fact that is not visible from the test in question either. We could improve the readability of the test by referring to the element as this.controller.view instead, but keeping the setView call inside the test probably yields the best readability. What other changes would you suggest to improve this test’s readability in stand-alone mode? 17.2 Tests as Behavior Specification When writing unit tests as part of test-driven development, we automatically treat tests as a specification mechanism—each test defines a distinct requirement and lays out the next goal to reach. Although we might want to occasionally pick up speed by introducing more code than “the smallest possible amount of test necessary to fail the test,” doing so inside one and the same test rarely is the best choice. 17.2.1 Test One Behavior at a Time Any given unit test should focus clearly on one specific behavior in the system. In most cases this can be directly related to the number of asserts, or if using mocks, expectations. Tests are allowed to have more than a single assert, but only when all the asserts logically test the same behavior. Listing 17.9 revisits a previous example of a test that uses three assertions to verify one behavior—that calling dispatch on the Comet client causes the observer to be notified of the right event and with the right data. Listing 17.9 Verifying one behavior with three asserts "test dispatch should notify observers": function () { var client = Object.create(ajax.cometClient); client.observers = { notify: stubFn() }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 17.2 Tests as Behavior Specification 469 client.dispatch({ someEvent: [{ id: 1234 }] }); var args = client.observers.notify.args; assert(client.observers.notify.called); assertEquals("someEvent", args[0]); assertEquals({ id: 1234 }, args[1]); } Testing only a single behavior in any given test means that when it fails, the source of failure will be obvious. This is a huge benefit because following this guideline will completely eradicate the need of a debugger to test the innards of a method. This single behavior focus also helps make the tests easier to understand. 17.2.2 Test Each Behavior Only Once Re-testing behaviors already covered in existing tests adds no value to the spec- ification of the system, neither does it help find bugs. It does, however, add to the maintenance burden. Testing the same behavior in more than one test means more tests to update whenever we want to change the behavior, and it means more tests will fail for the exact same reason, reducing the test case’s ability to pinpoint erroneous behavior. The most common source of duplicated verification comes from negligence; while testing each aspect of a method in dedicated tests, it is easy to inadvertently introduce an overlap between tests if we don’t pay close attention. Another possible reason for re-testing verified behavior is lack of trust. If we trust our tests, there is no reason to question a previous test’s validity by repeating an assertion. Listing 17.10 shows a test from Chapter 13, Streaming Data with Ajax and Comet, in which we expect the cometClient not to start polling a second time if connect has already been called once. Notice how the test simply assumes that the first call works as expected. The behavior of the first call is covered by other tests, and there is no need to assert that ajax.poll was called the first time. Listing 17.10 Assuming connect works the first time "test should not connect if connected": function () { this.client.url = "/my/url"; ajax.poll = stubFn({}); this.client.connect(); ajax.poll = stubFn({}); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 470 Writing Good Unit Tests this.client.connect(); assertFalse(ajax.poll.called); } Another less obvious source of re-testing the same behavior is covering browser inconsistencies in the wrong places. If you find yourself testing for DOM-related quirks inside a method whose purpose is not to cover the specific quirk, you need to move the offending code into a dedicated function. This way you can verify that performBuggyDOMRoutine handles all the DOM quirkiness properly across browsers, and simply verify that depending interfaces use this method. 17.2.3 Isolate Behavior in Tests When we test a single behavior at a time, pinpointing the source of error when tests fail is easy. However, discrepancies in indirect inputs may distort the results, causing tests to fail not because the targeted logic is faulty, but because it’s dependencies are behaving in unexpected ways. Back in Part I, Test-Driven Development, we referred to these kinds of tests as “accidental integration tests.” That sure sounds bad, but as we are about to discover, it does not need to be. 17.2.3.1 Isolation by Mocking and Stubbing One way to completely isolate a unit is to stub or mock all of its dependencies. Some people will tell you this is in fact the only way to properly isolate behavior. Throughout Part III, Real-World Test-Driven Development in JavaScript, there are lots of examples of tests that stub generously to isolate behavior. Listing 17.11, originally from Chapter 15, TDD and DOM Manipulation: The Chat Client, shows a test for the chat client message form controller that stubs all the objects that handleSubmit interacts with in order to verify that the message is published through the model object. Listing 17.11 Stubbing all dependencies TestCase("FormControllerHandleSubmitTest", { "test should publish message": function () { var controller = Object.create(messageController); var model = { notify: stubFn() }; controller.setModel(model); controller.handleSubmit(); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 17.2 Tests as Behavior Specification 471 assert(model.notify.called); assertEquals("message", model.notify.args[0]); assertObject(model.notify.args[1]); } }); Rather than performing state verification on the model object to verify that it received the given message, we stub the notify method and use behavior veri- fication to verify that it was called correctly. Tests for the cometClient verify that calling the method correctly will make sure the message is correctly sent to the server. 17.2.3.2 Risks Introduced by Mocks and Stubs In dynamic languages such as JavaScript, there is always a risk associated with test doubles. As an example, consider the test in Listing 17.12, which verifies that the form is not actually submitted when the user submits a message to the chat service. Listing 17.12 Verifying that the form submit action is aborted "test should prevent event default action": function () { this.controller.handleSubmit(this.event); assert(this.event.prevenDefault.called); } Having written this test, we have introduced a new requirement for the system under test. After confirming that it fails, we proceed to write the passing code. Once the test passes, we move on to the next behavior. Upon testing the resulting code in a browser, we will be shocked to find that the code throws an error when posting a message. The observant reader will already have noticed the problem; we accidentally misspelled preventDefault, leaving out the first “t.” Because the stub is in no way associated with a real exemplar of the kind of object we are faking, we have no safety net catching these kinds of errors for us. Languages like Java solve these kinds of problems with interfaces. Had the event stub been stated to implement the event interface, we would have realized our mistake, as the test would err because the stub did not implement preventDefault. Even if it did—e.g., through inheritance— the call to prevenDefault from production code would have erred because this method definitely isn’t part of the event interface. Introducing typos in method names may seem like a silly example, but it’s a simple illustration of a problem that can take a lot more obscure forms. In the From the Library of WoweBook.Com Download from www.eBookTM.com ptg 472 Writing Good Unit Tests case of the misspelled method name, you probably would notice the mistake either while initially writing it, during the initial run or, while writing it again in production code. If the mismatch between the test double and the real object was the wrong order or number of arguments passed to a method, it would not have been as obvious. While writing the code for the chat server originally covered in Chapter 14, Server-Side JavaScript with Node.js, I did in fact make such a mistake. In my ini- tial attempt at the controller’s get method, I made a mistake while constructing the expected output. As you might remember, the server was supposed to emit JSON responses that would work with the cometClient. Because my initial expectations deviated from the actual format used by the client object, the chat server did not work as expected upon finishing it, even though all the tests passed. The change to make it work was a simple one, but ideally we should avoid such mistakes. This is not to say you shouldn’t use stubs and mocks in your tests. They are effective tools, but need to be used with some care and attention. Always make sure to double check that your test doubles properly mirror the real deal. One way to achieve this is to use a real object as a starting point for the stub or mock. For instance, imagine a method like sinon.stubEverything(target), which could be used to create a stub object with stub function properties corresponding to all the methods of the target object. This way you take away the chance of using a fake method that doesn’t exist in production code. 17.2.3.3 Isolation by Trust Another way to isolate units is to make sure that any object the unit interacts with can somehow be trusted. Obviously, mocks and stubs can generally be trusted so long as they properly mirror the objects they mimic. Objects that are already tested can also be trusted. The same should be true for any third party library code in use. When dependencies are previously tested and known to work as expected, the chance of failing a test due to uncooperative dependencies is small enough to provide acceptable isolation. Although such tests can be considered “accidental integration tests,” they usu- ally integrate only a small group of objects, and do so in a controlled manner. The up side to using real objects is that we can use state verification, thus loosening the coupling between test code and production code. This gives us more room to refactor the implementation without having to change the tests, thus reducing the maintenance burden of the application as a whole. From the Library of WoweBook.Com Download from www.eBookTM.com . Helpers Another example of repeated patterns from Part III, Real-World Test-Driven Development in JavaScript, that could be simplified by a higher-level ab- straction is testing of event handlers. Given. abstract away compound verification. The most glaring example of this from Part III, Real-World Test-Driven Development in JavaScript, is the behavior verification of the stubs. Listing 17.4 shows. how. 17.1.1.2 Breaking Free of Technical Limitations All of the tests in Part III, Real-World Test-Driven Development in JavaScript, were written using libraries that consider any method whose

Ngày đăng: 04/07/2014, 22:20