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

Phát triển Javascript - part 43 pot

10 219 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,42 MB

Nội dung

ptg 15.2 The User Form 393 The next test, shown in Listing 15.5, expects setView to be a function. Listing 15.5 Expecting setView to be a function "test should have setView method": function () { assertFunction(userController.setView); } Listing 15.6 adds an empty method to pass the test. Listing 15.6 Adding an empty setView method (function () { function setView(element) {} tddjs.namespace("chat").userFormController = { setView: setView }; }()); 15.2.1.2 Adding a Class The first actual behavior we’ll test for is that the “js-chat” class name is added to the DOM element, as seen in Listing 15.7. Note that the test requires the Object.create implementation from Chapter 7, Objects and Prototypal Inheritance, in lib/object.js to run smoothly across browsers. Listing 15.7 Expecting the view to have its class name set TestCase("UserFormControllerSetViewTest", { "test should add js-chat class": function () { var controller = Object.create(userController); var element = {}; controller.setView(element); assertClassName("js-chat", element); } }); The first thing that sticks out about this test is that it contains no DOM elements. It does, however, use the assertClassName assertion,which checks if an element has the given class name. This assertion is generic, and only checks that the object defines a string property className and that one of its space separated values matches the provided string. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 394 TDD and DOM Manipulation: The Chat Client The element object is a simple stub object. At this point there’s no need to use a real DOM element, because all we want to check is that some property was assigned. The test fails, and Listing 15.8 assigns the class name to pass it. Listing 15.8 Adding the class function setView(element) { element.className = "js-chat"; } At this point you might worry about a few things. Should we really be overriding the class name like that? Should the class name not be configurable? Remember: You ain’t gonna need it! At this point, we have no use case demonstrating the need to use an element that already has class names or the need to use another class name than “js-chat.” Once we have, we can jot down a few tests to document those requirements, and then we can implement them. Right now we don’t need them, and they’ll just be slowing us down. 15.2.1.3 Adding an Event Listener Next we will add an event listener to the form’s “submit” event. To do this, we will use the tddjs.dom.addEventHandler interface we wrote in Chapter 10, Feature Detection. Testing event handlers is commonly accepted as a challenging task. The main reasons being that triggering user events from script in a cross- browser manner is less than trivial, and tests need a lot of setup, thus they can easily become complex. Unit testing event handlers in application code, when done right, is in fact trivial. Attaching event handlers through an abstraction such as tddjs. dom.addEventHandler means that all we need to assert is that this method is called correctly. By stubbing it, we gain access to the arguments passed to it, which means that we can manually call the handler to test the behavior of the event handler (in another dedicated test case). Tests that rely heavily on event data, such as mouse coordinates, neighbouring elements, and less tangible data may require com- plex setup, but such setup can be hidden behind, e.g., a fake event implementation for use in tests. I’m not saying that you should not test event handlers end-to-end, but I am saying that the application unit test suite is unlikely the right place to do so. First, one would hope that whatever library is being used to add event listeners has its own comprehensive test suite, meaning that in your tests you should be able to trust it. Second, if your application has acceptance tests, or some kind of in-browser From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.2 The User Form 395 integration tests, those are good places to test end-to-end functionality, including DOM event handlers. We will return briefly to this topic in Chapter 17, Writing Good Unit Tests. As there is no need to add an actual DOM event listener while testing, we can simply stub addEventHandler in the tests. Listing 15.9 shows the first test. Listing 15.9 Expect the element’s submit event to be handled "test should handle submit event": function () { var controller = Object.create(userController); var element = {}; var dom = tddjs.namespace("dom"); dom.addEventHandler = stubFn(); controller.setView(element); assert(dom.addEventHandler.called); assertSame(element, dom.addEventHandler.args[0]); assertEquals("submit", dom.addEventHandler.args[1]); assertFunction(dom.addEventHandler.args[2]); } As we have not yet included addEventHandler as a dependency, we use the namespace method to retrieve or define the dom namespace before stubbing the addEventHandler method. The test fails, and Listing 15.10 adds the method call to pass it. Listing 15.10 Adding a submit event handler var dom = tddjs.namespace("dom"); function setView(element) { element.className = "js-chat"; dom.addEventHandler(element, "submit", function () {}); } Once again, we use the namespace method to avoid trouble. Using local aliases to reduce typing and speed up identifier resolution is useful, but also causes objects to be cached before we use them. Because the source files are loaded first, the tddjs.dom object is not available when we assign it to the local dom variable. However, by the time the test triggers the call to dom.addEventHandler, the test has filled in the blanks. Using the namespace method means both files refer to the same object without us having to worry about which one loaded first. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 396 TDD and DOM Manipulation: The Chat Client Running the test produces some disappointing results. The test passes, but unfortunately the previous test now breaks, as there is no addEventHandler method around at the point of running it. We can fix this and the duplicated test code by elevating some common code into a setUp method, as Listing 15.11 shows. Listing 15.11 Extracting code into setUp /* */ var dom = tddjs.namespace("dom"); /* */ TestCase("UserFormControllerSetViewTest", { setUp: function () { this.controller = Object.create(userController); this.element = {}; dom.addEventHandler = stubFn(); }, "test should add js-chat class": function () { this.controller.setView(this.element); assertClassName("js-chat", this.element); }, "test should handle submit event": function () { this.controller.setView(this.element); assert(dom.addEventHandler.called); assertSame(this.element, dom.addEventHandler.args[0]); assertEquals("submit", dom.addEventHandler.args[1]); assertFunction(dom.addEventHandler.args[2]); } }); Even though both tests use setView in the same way, we keep it out of setUp, because this call is not part of the setup, rather it is the exercise step of the test. Refactoring the test got the tests back on track, and they now both pass. For the next test, we need to verify that the event handler is bound to the controller object. To achieve this we need stubFn to record the value of this at call time. Listing 15.12 shows the updated function. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.2 The User Form 397 Listing 15.12 Recording this in stubFn function stubFn(returnValue) { var fn = function () { fn.called = true; fn.args = arguments; fn.thisValue = this; return returnValue; }; fn.called = false; return fn; } The next test, seen in Listing 15.13, uses the improved stubFn to assert that the event handler is the controller’s handleSubmit method, readily bound to the controller object. Listing 15.13 Expecting the event handler to be handleSubmit bound to controller "test should handle event with bound handleSubmit": function () { var stub = this.controller.handleSubmit = stubFn(); this.controller.setView(this.element); dom.addEventHandler.args[2](); assert(stub.called); assertSame(this.controller, stub.thisValue); } This test shows another reason for not elevating the setView call to the setUp method; here we need additional setup before calling it, to be sure the method uses the stubbed handleSubmit method—not the original one, which would fail our test indefinitely. Listing 15.14 updates the call to pass the test. Note that the implementation requires the bind implementation from Chapter 6, Applied Functions and Closures, in lib/function.js. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 398 TDD and DOM Manipulation: The Chat Client Listing 15.14 Binding handleSubmit as event handler function setView(element) { element.className = "js-chat"; var handler = this.handleSubmit.bind(this); dom.addEventHandler(element, "submit", handler); } We now pass the current test but again fail previous tests. The reason is that the controller does not actually define a handleSubmit method; thus, any test that does not stub it fails. The fix is easy enough; define the method on the controller. Listing 15.15 to the rescue. Listing 15.15 Adding an empty handleSubmit method /* */ function handleSubmit(event) { } tddjs.namespace("chat").userFormController = { setView: setView, handleSubmit: handleSubmit }; That’s the happy path for setView. It should also do basic error checking, at the very least verify that it receives an argument. I’ll leave doing so as an exercise. 15.2.2 Handling the Submit Event When the user submits the form, the handler should grab the value from the form’s first input element whose type is text, assign it to the model’s currentUser property, and then remove the “js-chat” class name, signifying end of life for the user component. Last, but not least, the handler needs to abort the event’s default action to avoid the browser actually posting the form. 15.2.2.1 Aborting the Default Action We’ll start with that last requirement; the event’s default action should be prevented. In standards compliant browsers, this is done by calling the preventDefault method on the event object as Listing 15.16 shows. Internet Explorer does not support this method, and rather requires the event handler to return false. However, as you might remember, addEventHandler from Chapter 10, Feature Detection, takes care of some basic event normalization to smooth things over for us. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.2 The User Form 399 Listing 15.16 Expecting the event’s preventDefault method to be called TestCase("UserFormControllerHandleSubmitTest", { "test should prevent event default action": function () { var controller = Object.create(userController); var event = { preventDefault: stubFn() }; controller.handleSubmit(event); assert(event.preventDefault.called); } }); Again we put our trust in a stubbed function. Passing this test requires a single line of added code, as Listing 15.17 shows. Listing 15.17 Preventing the default action function handleSubmit(event) { event.preventDefault(); } Now that the test passes, we can start worrying about duplicating setup between the two test cases. As usual, we’ll simply extract the setup to a local function that both test cases can share, as Listing 15.18 shows. Listing 15.18 Sharing setup function userFormControllerSetUp() { this.controller = Object.create(userController); this.element = {}; dom.addEventHandler = stubFn(); } TestCase("UserFormControllerSetViewTest", { setUp: userFormControllerSetUp, /* */ }); TestCase("UserFormControllerHandleSubmitTest", { setUp: userFormControllerSetUp, "test should prevent event default action": function () { var event = { preventDefault: stubFn() }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 400 TDD and DOM Manipulation: The Chat Client this.controller.handleSubmit(event); assert(event.preventDefault.called); } }); 15.2.2.2 Embedding HTML in Tests Next up is verifying that the model is updated with the username as entered in an input element. How will we provide an input element in the test? Basically we have two choices; continue stubbing, e.g., by giving the stub element a stub getElementsByTagName method, which returns a stub input element, or embed some markup in the test. Although the former approach works and allows us to completely control both direct and indirect inputs to the method under test, it increases the risk of stubs not matching reality, and for anything other than trivial cases requires us to write a whole lot of stubs. Embedding some markup in the test will keep the tests closer to the production environment, and at the same time requires less manual stub- bing. Additionally, by adding the user form inside the test case, the test case better documents how to use the controller. JsTestDriver provides two ways to include HTML in a test; in-memory elements and elements added to the document. Listing 15.19 shows a test that creates some HTML that is not attached to the document. Listing 15.19 Embedding HTML in a JsTestDriver test "test should embed HTML": function () { /*:DOC element = <div></div> */ assertEquals("div", this.element.tagName.toLowerCase()); } As you can see, the name before the equals sign names the property JsTestDriver should assign the resulting DOM element to. It’s important to note that the right side of the equals sign needs to nest elements inside a single root element. It can contain an arbitrarily complex structure, but there can only be one root node. The other way to include HTML in tests is by appending to the document, as Listing 15.20 illustrates. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.2 The User Form 401 Listing 15.20 Appending elements to the document "test should append HTML to document": function () { /*:DOC += <div id="myDiv"></div> */ var div = document.getElementById("myDiv"); assertEquals("div", div.tagName.toLowerCase()); } For the most part, not appending to the document is both slightly faster and more convenient, because JsTestDriver automatically assigns it to a property on the test case. Unless we need to pick up elements globally (e.g., by selecting them from the document) or need elements to render, there usually is no gain in appending the elements to the document. 15.2.2.3 Getting the Username Returning to the controller, the problem at hand is expecting handleSubmit to pick up what the user entered in the form’s first text input field and using it as the username. To do this, we’ll first remove the element stub we’ve been using so far, and use an actual form instead. Listing 15.21 shows the updated setUp. Listing 15.21 Embedding a user form in setUp function userFormControllerSetUp() { /*:DOC element = <form> <fieldset> <label for="username">Username</label> <input type="text" name="username" id="username"> <input type="submit" value="Enter"> </fieldset> </form> */ this.controller = Object.create(userController); dom.addEventHandler = stubFn(); } Running the test confirms that we’re still in the green. With an actual form in place, we can add the test that expects handleSubmit to read the input field, as seen in Listing 15.22. Listing 15.22 Expecting handleSubmit to read username from field "test should set model.currentUser": function () { var model = {}; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 402 TDD and DOM Manipulation: The Chat Client var event = { preventDefault: stubFn() }; var input = this.element.getElementsByTagName("input")[0]; input.value = "cjno"; this.controller.setModel(model); this.controller.setView(this.element); this.controller.handleSubmit(event); assertEquals("cjno", model.currentUser); } The test adds a stub model object with the so far non-existent setModel method. The fact that the method is missing causes the test to fail, so Listing 15.23 adds the method. Listing 15.23 Adding setModel /* */ function setModel(model) { this.model = model; } tddjs.namespace("chat").userFormController = { setView: setView, setModel: setModel, handleSubmit: handleSubmit }; One could argue that a simple setter such as this is superfluous, but providing setView and setModel methods makes the interface consistent and predictable. When ECMAScript 5 becomes widely supported, we can do one better by using native setters, which untangles the explicit method calls. Next up, we need to make the handleSubmit method actually pick up the current value of the input field. Listing 15.24 fills in the blanks. Listing 15.24 Picking up the username function handleSubmit(event) { event.preventDefault(); var input = this.view.getElementsByTagName("input")[0]; this.model.currentUser = input.value; } From the Library of WoweBook.Com Download from www.eBookTM.com . may require com- plex setup, but such setup can be hidden behind, e.g., a fake event implementation for use in tests. I’m not saying that you should not test event handlers end-to-end, but I am saying. or some kind of in-browser From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.2 The User Form 395 integration tests, those are good places to test end-to-end functionality,. { "test should add js-chat class": function () { var controller = Object.create(userController); var element = {}; controller.setView(element); assertClassName("js-chat", element); } }); The

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