ptg 12.6 Making POST Requests 283 Obviously, this method could be extended to properly encode arrays and other kinds of data as well. Because the encodeURIComponent function isn’t guaran- teed to be available, feature detection is used to conditionally define the method. 12.6.2.1 Encoding Data in ajax.request For post requests, data should be encoded and passed as an argument to the send method. Let’s start by writing a test that ensures data is encoded, as in Listing 12.58. Listing 12.58 Asserting data sent to post function setUp() { this.tddjsUrlParams = tddjs.util.urlParams; /* */ } function tearDown() { tddjs.util.urlParams = this.tddjsUrlParams; /* */ } TestCase("RequestTest", { /* */ "test should encode data": function () { tddjs.util.urlParams = stubFn(); var object = { field1: "13", field2: "Lots of data!" }; ajax.request("/url", { data: object, method: "POST" }); assertSame(object, tddjs.util.urlParams.args[0]); } }); Making this test pass isn’t so hard, as Listing 12.59 shows. Listing 12.59 Encoding data if any is available function request(url, options) { /* */ options = tddjs.extend({}, options); options.data = tddjs.util.urlParams(options.data); /* */ } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 284 Abstracting Browser Differences: Ajax We don’t need to check if data exists because urlParams was designed to handle a missing argument. Note that because the encoding interface was separated from the ajax interface, it would probably be a good idea to add a feature test for it. We could force such a feature test by writing a test that removed the method locally for the duration of the test and assert that the method did not throw an exception. I’ll leave that as an exercise. 12.6.2.2 Sending Encoded Data Next up is sending the data. For POST requests we want the data sent to send,as Listing 12.60 specifies. Listing 12.60 Expecting data to be sent for POST requests "test should send data with send() for POST": function () { var object = { field1: "$13", field2: "Lots of data!" }; var expected = tddjs.util.urlParams(object); ajax.request("/url", { data: object, method: "POST" }); assertEquals(expected, this.xhr.send.args[0]); } This test fails because we are force-feeding the send method null. Also note how we now trust tddjs.util.urlParams to provide the expected value. It should have its own set of tests, which should guarantee as much. If we are reluctant to trust it, we could stub it out to avoid it cluttering up the test. Some developers always stub or mock out dependencies such as this, and theoretically, not doing so makes the unit test slightly bend toward an integration test. We will discuss pros and cons of different levels of stubbing and mocking more extensively in Chapter 16, Mocking and Stubbing. For now, we will leave tddjs.util.urlParams live in our tests. To make the test pass we need to add data handling to ajax.request,as Listing 12.61 does. Listing 12.61 Initial attempt at handling data function request(url, options) { /* */ options = tddjs.extend({}, options); options.data = tddjs.util.urlParams(options.data); var data = null; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 12.6 Making POST Requests 285 if (options.method == "POST") { data = options.data; } /* */ transport.send(data); }; This is not optimal, but passes the test without failing the previous one that expects send to receive null. One way to clean up the ajax.request method is to refactor to extract the data handling, as Listing 12.62 shows. Listing 12.62 Extracting a data handling function function setData(options) { if (options.method == "POST") { options.data = tddjs.util.urlParams(options.data); } else { options.data = null; } } function request(url, options) { /* */ options = tddjs.extend({}, options); setData(options); /* */ transport.send(options.data); }; This somewhat obtrusively blanks data for GET requests, so we will deal with that immediately. 12.6.2.3 Sending Data with GET Requests Before we can move on to setting request headers we must make sure that it is possible to send data with GET requests as well. With GET, data is not passed to the send method, but rather encoded on the URL. Listing 12.63 shows a test specifying the behavior. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 286 Abstracting Browser Differences: Ajax Listing 12.63 Testing that GET requests can send data "test should send data on URL for GET": function () { var url = "/url"; var object = { field1: "$13", field2: "Lots of data!" }; var expected = url + "?" + tddjs.util.urlParams(object); ajax.request(url, { data: object, method: "GET" }); assertEquals(expected, this.xhr.open.args[1]); } With this test in place we need to modify the data processing. For both GET and POST requests we need to encode data, but for GET requests the data goes on the URL, and we must remember to still pass null to the send method. At this point we have enough requirements to make keeping them all in our heads a confusing affair. Tests are slowly becoming a fantastic asset; because we don’t need to worry about requirements we have already met, we can code along without being weighed down by ever-growing amounts of requirements. The implementation can be seen in Listing 12.64. Listing 12.64 Adding data to get requests function setData(options) { if (options.data) { options.data = tddjs.util.urlParams(options.data); if (options.method == "GET") { options.url += "?" + options.data; options.data = null; } } else { options.data = null; } } function request(url, options) { /* */ options = tddjs.extend({}, options); options.url = url; setData(options); /* */ transport.open(options.method || "GET", options.url, true); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 12.6 Making POST Requests 287 /* */ transport.send(options.data); }; Because the data handling might include modifying the URL to embed data onto it, we added it to the options object and passed that to setData, as before. Obviously, the above solution will break down if the URL already has query param- eters on it. As an exercise, I urge you to test for such a URL and update setData as necessary. 12.6.3 Setting Request Headers The last thing we need to do in order to pass data is setting request headers. Head- ers can be set using the setRequestHeader(name, value) method. At this point adding in header handling is pretty straightforward, so I will leave doing that as an exercise. To test this you will need to augment the fakeXMLHttp- Request object to record headers set on it so you can inspect them from your tests. Listing 12.65 shows an updated version of the object you can use for this purpose. Listing 12.65 Adding a fake setRequestHeader method var fakeXMLHttpRequest = { open: stubFn(), send: stubFn(), setRequestHeader: function (header, value) { if (!this.headers) { this.headers = {}; } this.headers[header] = value; }, readyStateChange: function (readyState) { this.readyState = readyState; this.onreadystatechange(); } }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 288 Abstracting Browser Differences: Ajax 12.7 Reviewing the Request API Even though we didn’t walk through setting request headers, I want to show you the resulting ajax.request after implementing header handling (this is but one possible solution). The full implementation can be seen in Listing 12.66. Listing 12.66 The “final” version of tddjs.ajax.request tddjs.noop = function () {}; (function () { var ajax = tddjs.namespace("ajax"); if (!ajax.create) { return; } function isSuccess(transport) { var status = transport.status; return (status >= 200 && status < 300) || status == 304 || (tddjs.isLocal() && !status); } function requestComplete(options) { var transport = options.transport; if (isSuccess(transport)) { if (typeof options.success == "function") { options.success(transport); } } else { if (typeof options.failure == "function") { options.failure(transport); } } } function setData(options) { if (options.data) { options.data = tddjs.util.urlParams(options.data); if (options.method == "GET") { var hasParams = options.url.indexOf("?") >= 0; options.url += hasParams ? "&" : "?"; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 12.7 Reviewing the Request API 289 options.url += options.data; options.data = null; } } else { options.data = null; } } function defaultHeader(transport, headers, header, val) { if (!headers[header]) { transport.setRequestHeader(header, val); } } function setHeaders(options) { var headers = options.headers || {}; var transport = options.transport; tddjs.each(headers, function (header, value) { transport.setRequestHeader(header, value); }); if (options.method == "POST" && options.data) { defaultHeader(transport, headers, "Content-Type", "application/x-www-form-urlencoded"); defaultHeader(transport, headers, "Content-Length", options.data.length); } defaultHeader(transport, headers, "X-Requested-With", "XMLHttpRequest"); } // Public methods function request(url, options) { if (typeof url != "string") { throw new TypeError("URL should be string"); } options = tddjs.extend({}, options); options.url = url; setData(options); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 290 Abstracting Browser Differences: Ajax var transport = tddjs.ajax.create(); options.transport = transport; transport.open(options.method || "GET", options.url, true); setHeaders(options); transport.onreadystatechange = function () { if (transport.readyState == 4) { requestComplete(options); transport.onreadystatechange = tddjs.noop; } }; transport.send(options.data); } ajax.request = request; function get(url, options) { options = tddjs.extend({}, options); options.method = "GET"; ajax.request(url, options); } ajax.get = get; function post(url, options) { options = tddjs.extend({}, options); options.method = "POST"; ajax.request(url, options); } ajax.post = post; }()); The ajax namespace now contains enough functionality to serve most uses of asynchronous communication, although it is far from complete. Reviewing the implementation so far seems to suggest that refactoring to extract a request ob- ject as the baseline interface would be a good idea. Peeking through the code in Listing 12.66, we can spot several helpers that accept an options object. I’d sug- gest that this object in fact represents the state of the request, and might as well have been dubbed request at this point. In doing so, we could move the logic around, making the helpers methods of the request object instead. Following this train of thought possibly could lead our ajax.get and ajax.post implementations to look something like Listing 12.67. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 12.7 Reviewing the Request API 291 Listing 12.67 Possible direction of the request API (function () { /* */ function setRequestOptions(request, options) { options = tddjs.extend({}, options); request.success = options.success; request.failure = options.failure; request.headers(options.headers || {}); request.data(options.data); } function get(url, options) { var request = ajax.request.create(ajax.create()); setRequestOptions(request, options); request.method("GET"); request.send(url); }; ajax.get = get; function post(url, options) { var request = ajax.request.create(ajax.create()); setRequestOptions(request, options); request.method("POST"); request.send(url); }; ajax.post = post; }()); Here the request.create takes a transport as its only argument, meaning that we provide it with its main dependency rather than having it retrieve the object itself. Furthermore, the method now returns a request object that can be sent when configured. This brings the base API closer to the XMLHttpRequest object it’s wrapping, but still contains logic to set default headers, pre-process data, even out browser inconsistencies, and so on. Such an object could easily be extended in order to create more specific requesters as well, such as a JSONRequest. That object could pre-process the response as well, by for instance passing readily parsed JSON to callbacks. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 292 Abstracting Browser Differences: Ajax The test cases (or test suite if you will) built in this chapter provide some insight into the kind of tests TDD leaves you with. Even with close to 100% code coverage (every line of code is executed by the tests), we have several holes in tests; more tests for cases when things go wrong—methods receive the wrong kind of arguments, and other edge cases are needed. Even so, the tests document our entire API, provides decent coverage, and makes for an excellent start in regards to a more solid test suite. 12.8 Summary In this chapter we have used tests as our driver in developing a higher level API for the XMLHttpRequest object. The API deals with certain cross-browser is- sues, such as differing object creation, memory leaks, and buggy send methods. Whenever a bug was uncovered, tests were written to ensure that the API deals with the issue at hand. This chapter also introduced extensive use of stubbing. Even though we saw how stubbing functions and objects could easily be done manually, we quickly realized that doing so leads to too much duplication. The duplication prompted us to write a simple function that helps with stubbing. We will pick up on this idea in Chapter 16, Mocking and Stubbing, and solve the case we didn’t solve in this chapter; stubbing functions that will be called multiple times. Coding through tddjs.ajax.request and friends, we have refactored both production code and tests aggressively. Refactoring is perhaps the most valu- able tool when it comes to producing clean code and removing duplication. By frequently refactoring the implementation, we avoid getting stuck trying to come up with the greatest design at any given time—we can always improve it later, when we understand the problem better. As food for thought, we rounded off by discussing a refactoring idea to further improve the API. The end result of the coding exercise in this chapter is a usable, yet hardly complete, “ajax” API. We will use this in the next chapter, when we build an interface to poll the server and stream data. From the Library of WoweBook.Com Download from www.eBookTM.com . headers, "Content-Type", "application/x-www-form-urlencoded"); defaultHeader(transport, headers, "Content-Length", options.data.length); } defaultHeader(transport, headers, "X-Requested-With",. extract a request ob- ject as the baseline interface would be a good idea. Peeking through the code in Listing 12.66, we can spot several helpers that accept an options object. I’d sug- gest that this. developing a higher level API for the XMLHttpRequest object. The API deals with certain cross-browser is- sues, such as differing object creation, memory leaks, and buggy send methods. Whenever