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

Test Driven JavaScript Development- P15 docx

20 258 0
Tài liệu đã được kiểm tra trùng lặp

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

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 20
Dung lượng 185 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

Listing 12.44 Making sure the success handler is called for local requests "test should call success handler for local requests": function { this.xhr.readyState = 4; this.xhr.status = 0

Trang 1

requestComplete(transport, options);

transport.onreadystatechange = tddjs.noop;

} };

transport.send(null);

};

/* */

}());

Adding these two lines makes the tests pass again Re-running the massive re-quest integration test in Internet Explorer confirms that the memory leak is now gone

12.5.4 Local Requests

The last issue with the current implementation is that it is unable to make local requests Doing so results in no errors, yet “nothing happens.” The reason for this

is that the local file system has no concept of HTTP status codes, so the status code

is 0 when readyState is 4 Currently our implementation only accepts status code 200, which is insufficient in any case We will add support for local requests

by checking if the script is running locally and that the status code is not set, as the test in Listing 12.44 shows

Listing 12.44 Making sure the success handler is called for local requests

"test should call success handler for local requests":

function () { this.xhr.readyState = 4;

this.xhr.status = 0;

var success = stubFn();

tddjs.isLocal = stubFn(true);

ajax.get("file.html", { success: success });

this.xhr.onreadystatechange();

assert(success.called);

}

The test assumes a helper method tddjs.isLocal to check if the script is running locally Because we are stubbing it, a reference to it is saved in the setUp, allowing it to be restored in tearDown as we did before

Trang 2

To pass the test, we will call the success callback whenever the request is for a local file and the status code is not set Listing 12.45 shows the updated ready state

change handler

Listing 12.45 Allow local requests to succeed

function requestComplete(transport, options) {

var status = transport.status;

if (status == 200 || (tddjs.isLocal() && !status)) {

if (typeof options.success == "function") { options.success(transport);

} } }

The implementation passes the test In order to have this working in a browser

as well, we need to implement the helper that determines if the script is running

locally, as seen in Listing 12.46 Add it to the lib/tdd.js file

Listing 12.46 Checking current URL to decide if request is local

tddjs.isLocal = (function () {

function isLocal() { return !!(window.location &&

window.location.protocol.indexOf("file:") === 0);

} return isLocal;

}());

With this helper in place we can re-run the integration test locally, and observe that it now loads the HTML fragment

12.5.5 Testing Statuses

We finished another step—a test and a few lines of production code—it’s time to

review and look for duplication Even with the stub helpers we added previously,

the tests that verify behavior for different sets of readyState and status codes

look awfully similar And still we haven’t tested for other 2xx status codes, or any

error codes at all

To reduce the duplication, we will add a method to the fakeXMLHttp-Requestobject that allows us to fake its ready state changing Listing 12.47 adds a

method that changes the ready state and calls the onreadystatechange handler

Trang 3

Listing 12.47 Completing the fake request

var fakeXMLHttpRequest = { open: stubFn(),

send: stubFn(), readyStateChange: function (readyState) { this.readyState = readyState;

this.onreadystatechange();

} };

Using this method, we can extract a helper method that accepts as arguments

a status code and a ready state, and returns an object with properties success and failure, both indicating if the corresponding callback was called This is a bit of a leap because we haven’t yet written any tests for the failure callback, but in order to move along we will make a run for it Listing 12.48 shows the new helper function

Listing 12.48 Request helper for tests

function forceStatusAndReadyState(xhr, status, rs) { var success = stubFn();

var failure = stubFn();

ajax.get("/url", { success: success, failure: failure });

xhr.status = status;

xhr.readyStateChange(rs);

return { success: success.called, failure: failure.called };

}

Because this abstracts the whole body of a few tests, it was given a fairly verbose name so as to not take away from the clarity of the tests You’ll be the judge of whether the tests are now too abstract or still clear Listing 12.49 shows the helper

in use

Trang 4

Listing 12.49 Using the request helper in tests

"test should call success handler for status 200":

function () {

var request = forceStatusAndReadyState(this.xhr, 200, 4);

assert(request.success);

},

/* */

"test should call success handler for local requests":

function () {

tddjs.isLocal = stubFn(true);

var request = forceStatusAndReadyState(this.xhr, 0, 4);

assert(request.success);

}

When making big changes like this I like to introduce a few intentional bugs

in the helper to make sure it’s working as I expect For instance, we could

com-ment out the line that sets the success handler in the helper to verify that the test

then fails Also, the second test should fail if we comment out the line that stubs

tddjs.isLocalto return true, which it does Manipulating the ready state and

status code is also a good way to ensure tests still behave as expected

12.5.5.1 Further Status Code Tests

Using the new helper makes testing for new status codes a trivial task, so I will leave

it as an exercise Although testing for more status codes and making sure the failure

callback is fired for status codes outside the 200 range (with the exception of 0 for

local files and 304 “Not Modified”) is a good exercise in test-driven development,

doing so will add little new to our discussion I urge you to run through the steps as

an exercise, and when you are done you could always compare your quest to mine

by downloading the sample code off the book’s website2

2 http://tddjs.com

Trang 5

Listing 12.50 shows the resulting handler

Listing 12.50 Dispatching success and failure callbacks

function isSuccess(transport) { var status = transport.status;

return (status >= 200 && status < 300) ||

status == 304 ||

(tddjs.isLocal() && !status);

} function requestComplete(transport, options) {

if (isSuccess(transport)) {

if (typeof options.success == "function") { options.success(transport);

} } else {

if (typeof options.failure == "function") { options.failure(transport);

} } }

12.6 Making POST Requests

With the GET requests in a fairly usable state we will move on to the subject of POST requests Note that there is still a lot missing from the GET implementation, such as setting request headers and exposing the transport’s abort method Don’t worry, test-driven development is all about incrementally building an API, and given

a list of requirements to meet we can choose freely which ones makes sense to work

on at any given time Implementing POST requests will bring about an interesting refactoring, which is the motivation for doing this now

12.6.1 Making Room for Posts

The current implementation does not lend itself easily to support new HTTP verbs

We could pass the method as an option, but where? To the ajax.get method?

That wouldn’t make much sense We need to refactor the existing implementation

in three ways: First we need to extract a generic ajax.request method; then

we need to make the HTTP verb configurable Last, to remove duplication we will

Trang 6

“nuke” the body of the ajax.get method, leaving it to delegate its work to

ajax.request, forcing a GET request

12.6.1.1 Extracting ajax.request

Extracting the new method isn’t magic; simply copy-paste ajax.get and rename

it, as seen in Listing 12.51

Listing 12.51 Copy-pasting ajax.get to ajax.request

function request(url, options) {

// Copy of original ajax.get function body }

ajax.request = request;

Remember to run the tests after each step while refactoring In this case, only a copy-paste mistake resulting in a syntax error could possibly break the code because

the new method isn’t being called yet

12.6.1.2 Making the Method Configurable

Next up is to make the request method a configurable option on the ajax

request method This is new functionality and so requires a test, as seen in

Listing 12.52

Listing 12.52 Request method should be configurable

function setUp() {

this.tddjsIsLocal = tddjs.isLocal;

this.ajaxCreate = ajax.create;

this.xhr = Object.create(fakeXMLHttpRequest);

ajax.create = stubFn(this.xhr);

}

function tearDown() {

tddjs.isLocal = this.tddjsIsLocal;

ajax.create = this.ajaxCreate;

}

TestCase("GetRequestTest", {

setUp: setUp, tearDown: tearDown, /* */

});

Trang 7

TestCase("ReadyStateHandlerTest", { setUp: setUp,

tearDown: tearDown, /* */

});

TestCase("RequestTest", { setUp: setUp,

tearDown: tearDown,

"test should use specified request method": function () { ajax.request("/uri", { method: "POST" });

assertEquals("POST", this.xhr.open.args[0]);

} });

We add a new test case for the ajax.request method This makes three test cases using the same setup and teardown methods, so we extract them as functions inside the anonymous closure to share them across test cases

The test asserts that the request method uses POST as the request method when specified to do so The choice of method is not coincidental When TDD-ing, we should always add tests that we expect to fail somehow, tests that signify progress Using POST also forces us to produce a real solution, as hard-coding POST would make one of the other tests fail This is another quality mark of a unit test suite; breaking fundamental behavior in production code only results in one (or

a few) breaking tests This indicates tests are distinct and don’t retest already tested behavior

Onwards to a solution Listing 12.53 shows how ajax.request could make the request method a configuration option

Listing 12.53 Making the method configurable

function request(url, options) { /* */

transport.open(options.method || "GET", url, true);

/* */

}

That’s really all there is to it Tests are passing

Trang 8

12.6.1.3 Updating ajax.get

Now to the actual refactoring ajax.request now does the same job as

ajax.get, only slightly more flexible This means that all ajax.get really needs

to do is to make sure the method used is GET and let ajax.request do all the

work Listing 12.54 shows the spiffy new ajax.get

Listing 12.54 Cropping ajax.get’s body

function get(url, options) {

options = tddjs.extend({}, options);

options.method = "GET";

ajax.request(url, options);

}

As we are now overriding the method option, we use the tddjs.extend

method from Chapter 7, Objects and Prototypal Inheritance, to make a copy of the

optionsobject before making changes to it Running the tests confirms that this

works as expected, and voila, we have a foundation for the post method

Now that the interface changed, our tests are in need of some maintenance Most tests now target ajax.get while actually testing the internals of ajax.request

As we discussed as early as in Chapter 1, Automated Testing, voila, this kind of

indirection in tests is generally not appreciated Unit tests need maintenance as

much as production code, and the key to avoiding that becoming a problem is

dealing with these cases as they arise In other words, we should update our test

cases immediately

Fortunately, housekeeping is simple at this point All the tests except “should define get method” can be moved from GetRequestTest to RequestTest The only

modification we need to make is to change all calls to get to request directly

The tests for the ready state change handler already have their own test case,

Ready-StateHandlerTest In this case we only need to update the method calls from get

to request This includes the call inside the forceStatusAndReadyState

helper

Moving tests, changing method calls, and re-running the tests takes about half

a minute, no big deal In more complex situations, such changes may be more

involved, and in those cases some folks feel it’s a good idea to employ more test

helpers to avoid coupling the tests too tightly to the interface being tested I think

this practice takes away some of the value of tests as documentation, and I use it

sparingly

Trang 9

12.6.1.4 Introducing ajax.post

With ajax.request in place, implementing POST requests should be a breeze

Feeling brave, we skip the simple test to prove the method’s existence this time around Instead, the test in Listing 12.55 shows how we expect the method to behave

Listing 12.55 Expecting ajax.post to delegate to ajax.request

TestCase("PostRequestTest", { setUp: function () {

this.ajaxRequest = ajax.request;

}, tearDown: function () { ajax.request = this.ajaxRequest;

},

"test should call request with POST method": function () { ajax.request = stubFn();

ajax.post("/url");

assertEquals("POST", ajax.request.args[1].method);

} });

Implementation is trivial, as seen in Listing 12.56

Listing 12.56 Delegating ajax.post to ajax.request with POST as method

function post(url, options) { options = tddjs.extend({}, options);

options.method = "POST";

ajax.request(url, options);

} ajax.post = post;

Running the tests confirms that this implementation solves the newly added requirement As always, we look for duplication before moving on Obviously, the getand post methods are very similar We could extract a helper method, but

saving only two lines in two methods at the expense of another function call and another level of indirection doesn’t seem worthwhile at this point You may feel differently

Trang 10

12.6.2 Sending Data

In order for the POST request to make any sense, we need to send data with it

To send data to the server the same way a browser posts a form we need to do

two things: encode the data using either encodeURI or encodeURIComponent

(depending on how we receive the data) and set the Content-Type header We will

start with the data

Before we head into the request test case to formulate a test that expects encoded data, let’s take a step back and consider what we are doing Encoding strings isn’t a

task unique to server requests; it could be useful in other cases as well This insight

points in the direction of separating string encoding into its own interface I won’t

go through the steps required to build such an interface here; instead Listing 12.57

shows a very simple implementation

Listing 12.57 Simplified url parameter encoder

(function () {

if (typeof encodeURIComponent == "undefined") { return;

} function urlParams(object) {

if (!object) { return "";

}

if (typeof object == "string") { return encodeURI(object);

} var pieces = [];

tddjs.each(object, function (prop, val) { pieces.push(encodeURIComponent(prop) + "=" +

encodeURIComponent(val));

});

return pieces.join("&");

} tddjs.namespace("util").urlParams = urlParams;

}());

Trang 11

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);

/* */

}

Ngày đăng: 03/07/2014, 05:20

TỪ KHÓA LIÊN QUAN