ptg 13.1 Polling for Data 303 13.1.3 Testing Timers JsTestDriver does not do asynchronous tests, so we need some other way of test- ing use of timers. There is basically two ways of working with timers. The ob- vious approach is stubbing them as we have done with ajax.request and ajax.create (or in a similar fashion). To stub them easily within tests, stub the window object’s setTimeout property, as seen in Listing 13.14. Listing 13.14 Stubbing setTimeout (function () { TestCase("ExampleTestCase", { setUp: function () { this.setTimeout = window.setTimeout; }, tearDown: function () { window.setTimeout = this.setTimeout; }, "test timer example": function () { window.setTimeout = stubFn(); // Setup test assert(window.setTimeout.called); } }); }()); JsUnit, although not the most modern testing solution around (as discussed in Chapter 3, Tools of the Trade), does bring with it a few gems. One of these is jsUnitMockTimeout.js, a simple library to aid testing of timers. Note that although the file is named “mock,” the helpers it defines are more in line with what we have been calling stubs. jsUnitMockTimeout provides a Clock object and overrides the native setTimeout, setInterval, clearTimeout, and clearInterval func- tions. When Clock.tick(ms) is called, any function scheduled to run sometime within the next ms number of milliseconds will be called. This allows the test to effectively fast-forward time and verify that certain functions were called when scheduled to. The nice thing about the JsUnit clock implementation is that it makes tests focus more clearly on the expected behavior rather than the actual implementation—do some work, pass some time, and assert that some functions were called. Contrast From the Library of WoweBook.Com Download from www.eBookTM.com ptg 304 Streaming Data with Ajax and Comet this to the usual stubbing approach in which we stub the timer, do some work and then assert that the stub was used as expected. Stubbing yields shorter tests, but using the clock yields more communicative tests. We will use the clock to test the poller to get a feel of the difference. The jsUnitMockTimeout.js can be downloaded off the book’s website. 2 Copy it into the project’s lib directory. 13.1.3.1 Scheduling New Requests In order to test that the poller schedules new requests we need to: • Create a poller with a URL • Start the poller • Simulate the first request completing • Stub the send method over again • Fast-forward time the desired amount of milliseconds • Assert that the send method is called a second time (this would have been called while the clock passed time) To complete the request we will add yet another helper to the fakeXML- HttpRequest object, which sets the HTTP status code to 200 and calls the on- readystatechange handler with ready state 4. Listing 13.15 shows the new method. Listing 13.15 Adding a helper method to complete request var fakeXMLHttpRequest = { /* */ complete: function () { this.status = 200; this.readyStateChange(4); } }; Using this method, Listing 13.16 shows the test following the above require- ments. 2. http://tddjs.com From the Library of WoweBook.Com Download from www.eBookTM.com ptg 13.1 Polling for Data 305 Listing 13.16 Expecting a new request to be scheduled upon completion "test should schedule new request when complete": function () { var poller = Object.create(ajax.poller); poller.url = "/url"; poller.start(); this.xhr.complete(); this.xhr.send = stubFn(); Clock.tick(1000); assert(this.xhr.send.called); } The second stub deserves a little explanation. The ajax.request method used by the poller creates a new XMLHttpRequest object on each request. How can we expect that simply redefining the send method on the fake instance will be sufficient? The trick is the ajax.create stub—it will be called once for each request, but it always returns the same instance within a single test, which is why this works. In order for the final assert in the above test to succeed, the poller needs to fire a new request asynchronously after the original request finished. To implement this we need to schedule a new request from within the com- plete callback, as seen in Listing 13.17. Listing 13.17 Scheduling a new request function start() { if (!this.url) { throw new TypeError("Must specify URL to poll"); } var poller = this; ajax.request(this.url, { complete: function () { setTimeout(function () { poller.start(); }, 1000); } }); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 306 Streaming Data with Ajax and Comet Running the tests verifies that this works. Note that the way the test was written will allow it to succeed for any interval smaller than 1,000 milliseconds. If we wanted to ensure that the delay is exactly 1,000, not any value below it, we can write another test that ticks the clock 999 milliseconds and asserts that the callback was not called. Before we move on we need to inspect the code so far for duplication and other possible refactorings. All the tests are going to need a poller object, and seeing as there is more than one line involved in creating one, we will extract setting up the object to the setUp method, as seen in Listing 13.18. Listing 13.18 Extracting poller setup setUp: function () { /* */ this.poller = Object.create(ajax.poller); this.poller.url = "/url"; } Moving common setup to the right place enables us to write simpler tests while still doing the same amount of work. This makes tests easier to read, better at communicating their intent, and less prone to errors—so long as we don’t extract too much. Listing 13.19 shows the test that makes sure we wait the full interval. Listing 13.19 Making sure the full 1,000ms wait is required "test should not make new request until 1000ms passed": function () { this.poller.start(); this.xhr.complete(); this.xhr.send = stubFn(); Clock.tick(999); assertFalse(this.xhr.send.called); } This test passes immediately, as we already implemented the setTimeout call correctly. 13.1.3.2 Configurable Intervals The next step is to make the polling interval configurable. Listing 13.20 shows how we expect the poller interface to accept interval configuration. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 13.1 Polling for Data 307 Listing 13.20 Expecting the request interval to be configurable TestCase("PollerTest", { /* */ tearDown: function () { ajax.create = this.ajaxCreate; Clock.reset(); }, /* */ "test should configure request interval": function () { this.poller.interval = 350; this.poller.start(); this.xhr.complete(); this.xhr.send = stubFn(); Clock.tick(349); assertFalse(this.xhr.send.called); Clock.tick(1); assert(this.xhr.send.called); } }); This test does a few things different from the previous two tests. First of all, we add the call to Clock.reset in the tearDown method to avoid tests interfering with each other. Second, this test first skips ahead 349ms, asserts that the new re- quest was not issued, then leaps the last millisecond and expects the request to have been made. We usually try hard to keep each test focused on a single behavior, which is why we rarely make an assertion, exercise the code more, and then make another assertion the way this test does. Normally, I advise against it, but in this case both of the asserts contribute to testing the same behavior—that the new request is issued exactly 350ms after the first request finishes; no less and no more. Implementing the test is a simple matter of using poller.interval if it is a number, falling back to the default 1,000ms, as Listing 13.21 shows. Listing 13.21 Configurable interval function start() { /* */ var interval = 1000; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 308 Streaming Data with Ajax and Comet if (typeof this.interval == "number") { interval = this.interval; } ajax.request(this.url, { complete: function () { setTimeout(function () { poller.start(); }, interval); } }); } Running the tests once more yields that wonderful green confirmation of success. 13.1.4 Configurable Headers and Callbacks Before we can consider the poller somewhat complete we need to allow users of the object to set request headers and add callbacks. Let’s deal with the headers first. The test in Listing 13.22 inspects the headers passed to the fake XMLHttpRequest object. Listing 13.22 Expecting headers to be passed to request "test should pass headers to request": function () { this.poller.headers = { "Header-One": "1", "Header-Two": "2" }; this.poller.start(); var actual = this.xhr.headers; var expected = this.poller.headers; assertEquals(expected["Header-One"], actual["Header-One"]); assertEquals(expected["Header-Two"], actual["Header-Two"]); } This test sets two bogus headers, and simply asserts that they were set on the transport (and thus can safely be expected to be sent with the request). You may sometimes be tempted to skip running the tests before writ- ing the implementation—after all, we know they’re going to fail, right? While From the Library of WoweBook.Com Download from www.eBookTM.com ptg 13.1 Polling for Data 309 writing this test, I made a typo, accidentally writing var expected = this.xhr.headers. It’s an easy mistake to make. Running the test right away made me aware that something was amiss as the test was passing. Inspecting it one more time alerted me to the typo. Not running the test before writing the implementation would have made it impossible to discover the error. No matter how we had eventually implemented the headers, as long as it didn’t result in an exception or a syntax error, the test would have passed, lulling us into the false illusion that everything is fine. Always run tests after updating either the tests or the implementation! The implementation in Listing 13.23 is fairly mundane. Listing 13.23 Passing on the headers function start() { /* */ ajax.request(this.url, { complete: function () { setTimeout(function () { poller.start(); }, interval); }, headers: poller.headers }); } Next up, we want to ensure all the callbacks are passed along as well. We’ll start with the success callback. To test that it is passed we can use the complete method we added to the fake XMLHttpRequest object previously. This simulates a successful request, and thus should call the success callback. Listing 13.24 shows the test. Listing 13.24 Expecting the success callback to be called "test should pass success callback": function () { this.poller.success = stubFn(); this.poller.start(); this.xhr.complete(); assert(this.poller.success.called); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 310 Streaming Data with Ajax and Comet Implementing this is a simple matter of adding another line like the one that passed headers, as seen in Listing 13.25. Listing 13.25 Passing the success callback ajax.request(this.url, { /* */ headers: poller.headers, success: poller.success }); In order to check the failure callback the same way, we need to extend the fake XMLHttpRequest object. Specifically, we now need to simulate completing a request that failed in addition to the already implemented successful request. To do this we can make complete accept an optional HTTP status code argument, as Listing 13.26 shows. Listing 13.26 Completing requests with any status complete: function (status) { this.status = status || 200; this.readyStateChange(4); } Keeping 200 as thedefault status allowsus to makethis change withoutupdating or breaking any of the other tests. Now we can write a similar test and implemen- tation to require the failure callback to be passed. The test is listed in Listing 13.27 and implementation in Listing 13.28 Listing 13.27 Expecting the failure callback to be passed "test should pass failure callback": function () { this.poller.failure = stubFn(); this.poller.start(); this.xhr.complete(400); assert(this.poller.failure.called); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 13.1 Polling for Data 311 Listing 13.28 Passing the failure callback ajax.request(this.url, { /* */ headers: poller.headers, success: poller.success, failure: poller.failure }); The last thing to check is that the complete callback can be used by clients as well. Testing that it is called when the request completes is no different than the previous two tests, so I’ll leave doing so as an exercise. The implementation, however, is slightly different, as can be seen in Listing 13.29. Listing 13.29 Calling the complete callback if available ajax.request(this.url, { complete: function () { setTimeout(function () { poller.start(); }, interval); if (typeof poller.complete == "function") { poller.complete(); } }, /* */ }); 13.1.5 The One-Liner At this point the poller interface is in a usable state. It’s very basic, and lacks several aspects before it would be safe for production use. A glaring omission is the lack of request timeouts and a stop method, partly because timeouts and abort were also missing from the ajax.request implementation. Using what you have now learned you should be able to add these, guided by tests, and I urge you to give it a shot. Using these methods the poller could be improved to properly handle problems such as network issues. As promised in the introduction to this chapter, we will add a simple one-liner interface to go along with ajax.request, ajax.get and ajax.post. It will From the Library of WoweBook.Com Download from www.eBookTM.com ptg 312 Streaming Data with Ajax and Comet use the ajax.poller object we just built, which means that we can specify its behavior mostly in terms of a stubbed implementation of it. The first test will assert that an object inheriting from ajax.poller is created using Object.create and that its start method is called, as Listing 13.30 shows. Listing 13.30 Expecting the start method to be called TestCase("PollTest", { setUp: function () { this.request = ajax.request; this.create = Object.create; ajax.request = stubFn(); }, tearDown: function () { ajax.request = this.request; Object.create = this.create; }, "test should call start on poller object": function () { var poller = { start: stubFn() }; Object.create = stubFn(poller); ajax.poll("/url"); assert(poller.start.called); } }); This test case does the usual setup to stub and recover a few methods. By now, this wasteful duplication should definitely be rubbing you the wrong way. As mentioned in the previous chapter, we will have to live with it for now, as we will introduce better stubbing tools in Chapter 16, Mocking and Stubbing. Apart from the setup, the first test makes sure a new object is created and that its start method is called, and the implementation can be seen in Listing 13.31. Listing 13.31 Creating and starting a poller function poll(url, options) { var poller = Object.create(ajax.poller); poller.start(); } ajax.poll = poll; From the Library of WoweBook.Com Download from www.eBookTM.com . { "Header-One": "1", "Header-Two": "2" }; this.poller.start(); var actual = this.xhr.headers; var expected = this.poller.headers; assertEquals(expected["Header-One"], actual["Header-One"]); assertEquals(expected["Header-Two"], actual["Header-Two"]); } This. this.poller.headers; assertEquals(expected["Header-One"], actual["Header-One"]); assertEquals(expected["Header-Two"], actual["Header-Two"]); } This test sets two bogus headers, and simply. the request we will add yet another helper to the fakeXML- HttpRequest object, which sets the HTTP status code to 200 and calls the on- readystatechange handler with ready state 4. Listing 13.15