Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 20 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
20
Dung lượng
192,08 KB
Nội dung
ptg 15.6 The Final Chat Client 433 Listing 15.85 Expecting the message form to clear message "test should clear form after publish": function () { var el = this.element.getElementsByTagName("input")[0]; el.value = "NP: A vision of misery"; this.controller.handleSubmit(this.event); assertEquals("", el.value); } Ideally, we would not clear the form until we know for sure the message was sent. Unfortunately, the cometClient does not support adding a success callback at this point, so the best we can do is clearing it immediately after having sent it and hope for the best. The proper fix would include adding a third options argument to cometClient and wait for success. Listing 15.86 shows the message form controller’s updated handleSubmit. Listing 15.86 Clearing the message after publishing it function handleSubmit(event) { /* */ input.value = ""; } It would also be nice if the message form gave focus to the input field immedi- ately upon initializing it. I will leave doing so as an exercise. 15.6.2 Notes on Deployment Copy over the message form and message list controllers to chapp’s public di- rectory and reload your browser. The application should now be slightly smoother to use. Simply copying files to deploy them is cumbersome and error prone. Addi- tionally, serving the application with 15 individual script files is not optimal for performance. If you installed Ruby and RubyGems to use the jstestdriver and jsautotest tools in Chapter 3, Tools of the Trade, then you have a JavaScript and CSS concatenator and minifier at your fingertips. Listing 15.87 shows the three required commands to install Juicer, which will conveniently package your scripts for deployment. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 434 TDD and DOM Manipulation: The Chat Client Listing 15.87 Installing Juicer and YUI Compressor $ gem install juicer $ juicer install yui_compressor Run from the root of the Node.js application, the command in Listing 15.88 will produce a single file, chat.min.js, containing the entire client-side application. Listing 15.88 Using Juicer to compress files juicer merge -s -f -o public/js/chat.min.js \ public/js/function.js \ public/js/object.js \ public/js/tdd.js \ public/js/observable.js \ public/js/form_controller.js \ public/js/user_form_controller.js \ public/js/json2.js \ public/js/url_params.js \ public/js/ajax.js \ public/js/request.js \ public/js/poller.js \ public/js/comet_client.js \ public/js/message_list_controller.js \ public/js/message_form_controller.js \ public/js/chat_client.js The final result is a 14kB JavaScript file containing a fully operational chat room. Served with gzip compression, the total download should be about 5kB. Juicer is also able to find dependencies declared inside script files, meaning that we can jot down each file’s dependencies inside comments in them and then simply run “juicer merge chat.js” to produce the complete file, including the dependencies. More information on Juicer is available from the book’s website. 4 15.7 Summary In this chapter we have been able to pull together a lot of the code developed throughout this book to create a fully functional, entirely JavaScript based browser- based chat application. And we did it all using test-driven development, right from the very start. 4. http://tddjs.com From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 15.7 Summary 435 The key aspect of this chapter has been unit testing DOM manipulation, and structuring the outermost application layer in a sensible way. As we’ve discussed numerous times already, well factored software easily lends itself to unit testing, and the GUI—the DOM—is no exception to this rule. By employing the Model View Presenter/Passive View pattern, we were able to identify reusable components in the view and implement the chat client in a modular way, resulting in very loosely coupled modules that were easy to test in isolation. Developing these components using TDD was straightforward because each distinct unit had a well-defined responsibility. Dividing a hard problem into several smaller problems is a lot more manageable than trying to solve it all in one go. An interesting aspect about a pattern such as Model View Presenter is that there are numerous ways to apply it to the problem domain of client-side JavaScript. For instance, in many cases a portion of the DOM will represent the model because JavaScript widgets frequently manipulate the data already found on the page. The chat client was the final test-driven example, and we have reached the end of Part III, Real-World Test-Driven Development in JavaScript. In the final part of the book we’ll draw some lessons from the past five chapters as we dive deeper into stubbing and mocking, and finally identify some guidelines for writing good unit tests. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg This page intentionally left blank From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg Part IV Testing Patterns From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg This page intentionally left blank From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 16 Mocking and Stubbing W hile using test-driven development to develop five sample projects, we’ve become intimately familiar with the stubFn function. We have used it as a tool to both inspect interaction between objects, as well as isolating interfaces under test. But what exactly is a stub? We are about to find out as we dive a little deeper into the topic of using test doubles, objects that look like the real thing but really are bleak impersonations used to simplify tests. In this chapter we will look at the general theory of using test doubles, and get to know a few common types of test doubles a little better. Because we have already used stubs extensively in tests throughout Part III, Real-World Test-Driven Development in JavaScript, we will relate the discussion to previous examples. We will also look at a more capable stubbing and mocking library and see how such a thing can be used in place of stubFn and other homegrown helpers to simplify some of the tests we have written so far. 16.1 An Overview of Test Doubles A test double is an object that supports the same API, or at least the parts of it relevant to a given test, as the real thing, but does not necessarily behave the same way. Test doubles are used to both isolate interfaces and make tests more convenient; making tests faster, avoiding calls to inconvenient methods, or spying on method calls in place of assertions on direct or indirect output. 439 From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 440 Mocking and Stubbing The terminology used in this chapter is mostly adapted from Gerard Meszaros book “xUnit Test Patterns,” [7] slightly adjusted to the world of JavaScript. In addition to the names and definitions of different types of test doubles, I will use “system under test” to describe the code being tested. 16.1.1 Stunt Doubles Gerard Meszaros compares test doubles to Hollywood’s stunt doubles. Some movie scenes require dangerous stunts, physically demanding feats or other behavior that the leading actor is either not willing or able to perform. In such cases, a stunt double is hired to do the job. The stunt double need not be an accomplished actor, he simply needs to be able to catch on fire or fall off a cliff without being mortally wounded; and he needs to look somewhat like the leading actor, at least from a distance. Test doubles are just like stunt doubles. They take on the job when it’s incon- venient to use the leading star (production code); all we require from them is that the audience (system under test) cannot tell it apart from the real deal. 16.1.2 Fake Object The stubs we’ve been using aggressively throughout the example projects in Part III, Real-World Test-Driven Development in JavaScript, are one form of test doubles. They appear to behave like real objects, but their actions are pre-programmed to force a certain path through the system under test. Additionally, they record data about their interaction with other objects, available in the test’s verification stage. Another kind of test double is the fake object. A fake object provides the same functionality as the object it replaces and can be seen as an alternative implementa- tion, only its implementation is considerably simpler. For example, when working with Node.js the file system can easily become inconvenient from a testing perspec- tive. Constantly accessing it can make tests slower, and keeping a lot of test data on disk requires cleanup. We can alleviate these problems by implementing an in- memory file system that supports the same API as Node’s fs module and use this in tests. Fakes differ from stubs in that stubs are usually created and injected into the system from individual tests on a per-need basis. Fakes are more comprehensive replacements, and are usually injected into the system as a whole before running any tests. Tests are usually completely unaware of the fakes because they behave just like the objects they mirror, only significantly simplified. In the Node.js file system example we can imagine a complete implementation of the fs module as From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 16.2 Test Verification 441 an in-memory file system. The test setup can then make sure to place the fake implementation ahead of the built-in one on the load path. Neither individual tests nor production code will be aware that require("fs") actually loads a simplified in-memory file system. 16.1.3 Dummy Object A dummy object, as its name suggests, is usually just an empty object or function. When testing functions that expect several parameters, we are often only concerned with one of them at a time. If the function we’re testing throws errors for missing or wrongly typed arguments, we can pass it a dummy to “shut it up” while we focus on behavior not related to the argument in question. As an example,consider the test in Listing 16.1 from Chapter 15, TDD and DOM Manipulation: The Chat Client. The test verifies that the message list controller sets the element’s scrollTop equal to the value of its scrollHeight. However, the method also appends a new DOM element to the view element, and throws an exception if it does not have an appendChild method. For the purpose of this test we use a dummy to pass the test on appendChild to get to the behavior we want to test. Listing 16.1 Using a dummy function "test should scroll element down": function () { var element = { appendChild: stubFn(), scrollHeight: 1900 }; this.controller.setView(element); this.controller.addMessage({ user:"me",message:"Hey" }); assertEquals(1900, element.scrollTop); } 16.2 Test Verification Unit tests have four stages; setup, often divided between a shared setUp method and test specific configuration of objects; exercise, in which we call the function(s) to test; verification, in which we assert that the result of the exercise stage coincides with our expectations; and finally tear down, which never happens inside a test, but rather in a dedicated and shared tearDown method. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 442 Mocking and Stubbing Before we get into the nitty-gritty of stubs, mocks, and the difference between them, we will explore our options at the verification stage. As we will see shortly, veri- fication strategy is a central issue when making the choice between stubs and mocks. 16.2.1 State Verification Many of the tests in Part III, Real-World Test-Driven Development in JavaScript, determine success by asserting that certain objects have a specific state after some function was called. As an example, consider Listing 16.2 from Chapter 15, TDD and DOM Manipulation: The Chat Client, which expects the user form controller to set the currentUser property of the model object. It passes a dummy model object to the controller, and then inspects the object’s currentUser object to verify its behavior. Listing 16.2 Inspecting an object’s state to verify test "test should set model.currentUser": function () { var model = {}; 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 fact that the last line inspects a property of an object passed to the system under test to verify its success is called state verification. State verification leads to intuitive tests that clearly describe the outcome of using some part of the system. In this case, if the input field contains a username when the controller handles a submit event, we expect it to transfer this username to the model object’s currentUser property. The test does not say anything about how this should happen, thus it is completely detached from the implementation of handleSubmit. 16.2.2 Behavior Verification In many cases, testing the direct output of a test is not as simple as in Listing 16.2. For instance, keeping with the chat client example, the message form controller is in charge of publishing messages from the client to the server through the model object. Because there is no server in the tests, we cannot simply ask it for the message From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. [...]... also wrap the entire test case object in a call to sinon.testCase, which is the same as wrapping every test function in a call to sinon .test Listing 16.13 shows an example Listing 16.13 Automatically restoring stubs after each test TestCase("CometClientConnectTest", sinon.testCase({ setUp: function (stub) { /* */ stub(ajax, "poll").returns({}); }, "test connect should start polling": function () { this.client.connect();... recognize that the stubs in Part III, Real-World Test- Driven Development in JavaScript, have frequently been used this way; in fact, test spies are usually implemented as recording stubs 16.4.1 Testing Indirect Inputs The request interface we built in Chapter 12, Abstracting Browser Differences: Ajax, provides many examples of using test spies to verify a test The interface was built to provide a higher... side-by-side with the testing framework’s assertions JsTestDriver uses global assertions Listing 16.15 shows the necessary code for completely seamless integration Listing 16.15 Mixing Sinon’s assertions with the default JsTestDriver ones // Typically done in a global helper to share among // test cases sinon.assert.expose(this, true, false); TestCase("CometClientConnectTest", { /* */ "test connect should... the test function in a sinon .test call and using the stub method that is passed to it, stubs are strictly local and are automatically restored upon the test s completion, even if the test throws exceptions When using this feature we can throw out all the stub related logic in both the setUp and tearDown methods When you have a lot of tests that need this kind of clean up you can also wrap the entire test. .. runtime, such as Node.js Listing 16.16 shows a test from Chapter 14, Server-Side JavaScript with Node.js, in which we stub the getMessagesSince test to return a promise object Listing 16.16 Stubbing in Node.js var sinon = require("sinon"); /* */ testCase(exports, "chatRoom.waitForMessagesSince", { /* */ "should yield existing messages": sinon .test( function (test, stub) { var promise = new Promise();... object Listing 16.7 shows a test that verifies that requesting a URL causes the XMLHttpRequest object’s send method to be called Listing 16.7 Using a test spy to verify that a method is called on an indirect input TestCase("GetRequestTest", { setUp: function () { this.ajaxCreate = ajax.create; this.xhr = Object.create(fakeXMLHttpRequest); ajax.create = stubFn(this.xhr); }, /* */ "test should call send":... bit more hassle because we must make sure to restore the original method after running the test Listing 16.10 shows an extract of the setUp and tearDown methods of the Comet client test case along with a test using the stubFn method on the global ajax.poll Listing 16.10 Spying manually TestCase("CometClientConnectTest", { setUp: function () { this.client = Object.create(ajax.cometClient); this.ajaxPoll... "getMessagesSince").returns(promise); this.room.waitForMessagesSince(42).then(function (m) { test. same([{ id: 43 }], m); test. done(); }); }, /* */ }); Note that Sinon takes care not to override the test object that Nodeunit passes to the test The stub function is passed after any arguments passed to the function when it is called by the test runner Please purchase PDF Split-Merge on www.verypdf.com to remove this... ajax.create to return a fakeXMLHttp Request instance, which is assigned to the test for behavior verification The object returned from ajax.create is an indirect input to the ajax.request method Stubs or mocks are usually the only way to test the effects of an indirect input on the system under test 16.4.2 Inspecting Details about a Call A test spy need not restrict itself to recording whether or not a function...443 16.3 Stubs we expected it to receive To test this, we used a stub, as seen in Listing 16.3 Rather than inspecting some object’s state to verify its results, this test stubs the model’s publish method and then proceeds by asserting that it was called Listing 16.3 Inspecting a function’s behavior to verify test "test should publish message": function () { var controller = Object.create(messageController); . found on the page. The chat client was the final test- driven example, and we have reached the end of Part III, Real-World Test- Driven Development in JavaScript. In the final part of the book we’ll. in Part III, Real-World Test- Driven Development in JavaScript, have frequently been used this way; in fact, test spies are usually implemented as recording stubs. 16.4.1 Testing Indirect Inputs The. lot of tests that need this kind of clean up you can also wrap the entire test case object in a call to sinon.testCase, which is the same as wrapping every test function in a call to sinon .test.