Tải bản đầy đủ (.pdf) (10 trang)

Phát triển Javascript - part 42 ppsx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (2.4 MB, 10 trang )

ptg
14.6 Returning to the Controller
383
test.ok(this.res.writeHead.called);
test.equals(this.res.writeHead.args[0], 201);
test.done();
},
"should close connection": function (test) {
this.controller.respond(201);
test.ok(this.res.end.called);
test.done();
}
});
We can pass these tests by copying the two lines we last added to post into
the new respond method, as Listing 14.74 shows.
Listing 14.74 A dedicated respond method
var chatRoomController = {
/* */
respond: function (status) {
this.response.writeHead(status);
this.response.end();
}
};
Now we can simplify the post method by calling this method instead. Doing
so also allows us to merge the original tests for status code and connection closing,
by stubbing respond and asserting that it was called.
14.6.2.3 Formatting Messages
Next up for the get method is properly formatting messages. Again we’ll need to
lean on the cometClient, which defines the data format. The method should
respond with a JSON object whose properties name the topic and values are arrays
of objects. Additionally, the JSON object should include a token property. The


JSON string should be written to the response body.
We can formulate this as a test by stubbing respond as we did before, this
time expecting an object passed as the second argument. Thus, we will need to
embellish respond later, having it write its second argument to the response body
as a JSON string. Listing 14.75 shows the test.
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
384
Server-Side JavaScript with Node.js
Listing 14.75 Expecting an object passed to respond
function controllerSetUp() {
var req = this.req = new EventEmitter();
req.headers = { "x-access-token": "" };
/* */
var add = this.addMessagePromise = new Promise();
var wait = this.waitForMessagesPromise = new Promise();
this.controller.chatRoom = {
addMessage: stub(add),
waitForMessagesSince: stub(wait)
};
/* */
}
/* */
testCase(exports, "chatRoomController.respond", {
/* */
"should respond with formatted data": function (test) {
this.controller.respond = stub();
var messages = [{ user: "cjno", message: "hi" }];
this.waitForMessagesPromise.resolve(messages);

this.controller.get();
process.nextTick(function () {
test.ok(this.controller.respond.called);
var args = this.controller.respond.args;
test.same(args[0], 201);
test.same(args[1].message, messages);
test.done();
}.bind(this));
}
});
This test is a bit of a mouthful, and to make it slightly easier to digest, the setUp
method was augmented. All the tests so far have stubbed waitForMessagesS-
ince, and all of them require the headers to be set. Pulling these out makes it easier
to focus on what the test in question is trying to achieve.
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
14.6 Returning to the Controller
385
The test resolves the promise returned by waitForMessagesSince, and
expects the resolving data to be wrapped in a cometClient friendly object and
passed to the resolve method along with a 200 status. Listing 14.76 shows the
required code to pass the test.
Listing 14.76 Responding from get
get: function () {
var id = this.request.headers["x-access-token"] || 0;
var wait = this.chatRoom.waitForMessagesSince(id);
wait.then(function (msgs) {
this.respond(200, { message: msgs });
}.bind(this));

}
14.6.2.4 Updating the Token
Along with the messages, the get method needs to embed a token in its response.
The token will automatically be picked up by cometClient and sent with the
X-Access-Token header on subsequent requests. Listing 14.77 expects the token
to be passed along with the message.
Listing 14.77 Expecting a token embedded in the response
"should include token in response": function (test) {
this.controller.respond = stub();
this.waitForMessagesPromise.resolve([{id:24}, {id:25}]);
this.controller.get();
process.nextTick(function () {
test.same(this.controller.respond.args[1].token, 25);
test.done();
}.bind(this));
}
Passing the test involves passing the id of the last message as the token as seen
in Listing 14.78.
Listing 14.78 Embedding the token
get: function () {
/* */
wait.then(function (messages) {
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
386
Server-Side JavaScript with Node.js
this.respond(200, {
message: messages,
token: messages[messages.length - 1].id

});
}.bind(this));
}
14.6.3 Response Headers and Body
The final missing piece of the puzzle is encoding the response data as JSON and
writing the response body. I will leave TDD-ing these features into the respond
method as a last exercise for this chapter. For completeness, Listing 14.79 shows
one possible outcome of the respond method.
Listing 14.79 The respond method
respond: function (status, data) {
var strData = JSON.stringify(data) || "{}";
this.response.writeHead(status, {
"Content-Type": "application/json",
"Content-Length": strData.length
});
this.response.write(strData);
this.response.end();
}
And that’s it! To take the application for a spin, we can launch another command
line session, as Listing 14.80 shows.
Listing 14.80 Manually testing the finished app from the command line
$ node-repl
node> var msg = { user:"cjno", message:"Enjoying Node.js" };
node> var data = { topic: "message", data: msg };
node> var encoded = encodeURI(JSON.stringify(data));
node> require("fs").writeFileSync("chapp.txt", encoded);
node> Ctrl-d
$ curl -d `cat chapp.txt` http://localhost:8000/comet
$ curl http://localhost:8000/comet
{"message":[{"id":1,"user":"cjno",\

"message":"Enjoying Node.js"}],"token":1}
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
14.7 Summary
387
14.7 Summary
In this chapter we have gotten toknow Node.js, asynchronous I/O for V8 JavaScript,
and we have practiced JavaScript TDD outside the browser to see how the expe-
rience from previous exercises fares in a completely different environment than
we’re used to. By building a small web server to power a chat application we have
gotten to know Node’s HTTP, Assert, and Event APIs in addition to the third party
node-promise library.
To provide the application with data, we also built an I/O interface that first
mimicked Node’s conventional use of callbacks and later went through a detailed
refactoring exercise to convert it to use promises. Promises offer an elegant way
of working with asynchronous interfaces, and makes concurrency a lot easier, even
when we need to work with results in a predictable order. Promises are usable in any
JavaScript setting, and the Ajax tools seems particularly fit for this style of interface.
In the next chapter we will use the tools built in Chapter 12, Abstracting Browser
Differences: Ajax, and Chapter 13, Streaming Data with Ajax and Comet, to build
a client for the Node backend, resulting in a completely usable in-browser instant
chat application.
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
This page intentionally left blank
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg

15
TDD and DOM
Manipulation:
The Chat Client
D
eveloping client-side JavaScript includes a fair amount of DOM manipulation.
In this chapter we will use test-driven development to implement a client for the
chat backend we developed in Chapter 14, Server-Side JavaScript with Node.js. By
doing so we will see how to apply the techniques we have learned so far to test DOM
manipulation and event handling.
The DOM is an API just like any other, which means that testing it should be
fairly straightforward so long as we adhere to the single responsibility principle and
keep components loosely coupled. The DOM does potentially present a challenge
in that it consists entirely of host objects, but as we will see, we can work around
the potential problems.
15.1 Planning the Client
The task at hand is building a simple chat GUI. The resulting application will have
two views: when the user enters the application she will be presented with a form in
which to enter the desired username. Submitting the form will remove it and display
a list of messages and a form to enter new ones in its place. As usual, we will keep
the scope at a minimum to get through the entire exercise, so for example, there
will be no cookie management to remember the user. Throughout the chapter ideas
on how to add more features to the client will be suggested as exercises.
389
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
390
TDD and DOM Manipulation: The Chat Client
15.1.1 Directory Structure

Again, we will use JsTestDriver to run the tests. The client will eventually use all
the code developed throughout Part III, Real-World Test-Driven Development in
JavaScript, but we will start with a bare minimum and add in dependencies as they
are required. For the TDD session, some of the dependencies will always be stubbed,
meaning we won’t need them to develop the client. Listing 15.1 shows the initial
directory structure.
Listing 15.1 Initial directory structure
chris@laptop:~/projects/chat_client$ tree
.
| jsTestDriver.conf
| lib
| | stub.js
| ` tdd.js
| src
` test
stub.js contains the stubFn function from Chapter 13, Streaming Data with
Ajax and Comet, and tdd.js contains the tddjs object along with the various
tools built in Part II, JavaScript for Programmers, Listing 15.2 shows the contents
of the jsTestDriver.conf configuration file. As usual, you can download the
initial project state from the book’s website.
1
Listing 15.2 The initial JsTestDriver configuration
server: http://localhost:4224
load:
- lib/*.js
- src/*.js
- test/*.js
15.1.2 Choosing the Approach
Prior to launching the TDD session we need a general idea on how we’re going
to build the client. Our main priorities are to keep a clear separation between the

DOM and the data (provided by cometClient from Chapter 13, Streaming Data
1.
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
15.1 Planning the Client
391
with Ajax and Comet) and to control all dependencies from the outside, i.e., using
dependency injection. To achieve this we will employ a derivative oftheModel-View-
Controller (MVC) design pattern frequently referred to as Model-View-Presenter
(MVP), which is very well suited to facilitate unit testing and fits well with test-driven
development.
15.1.2.1 Passive View
MVP is practiced in a variety of ways and we will apply it in a manner that leans to-
ward what Martin Fowler, renowned programmer, author, and thinker, calls Passive
View. In this model, the view notifies the presenter—controller in Passive View—
of user input, and the controller completely controls the state of the view. The
controller responds to events in the view and manipulates the underlying model.
In a browser setting, the DOM is the view. For the chat application the model
will be provided by the cometClient object, and our main task is to develop the
controllers. Note the plural form; there are many controllers, each discrete widget
or even widget component can be represented by its own view and controller,
sometimes referred to as an MVP axis. This makes it easy to adhere to the single
responsibility principle, in which each object has one well-defined task. Throughout
this chapter we will refer to a controller/view duo as a component.
We will divide the chat client into three distinct components: the user form, the
message list, and the message form. The message list and form will not be displayed
until the user form is successfully completed. However, this flow will be controlled
from the outside, as controllers will not be aware of other controllers. Keeping them
completely decoupled means we can more easily manipulate the client by adding or

removing components, and it makes them easier to test.
15.1.2.2 Displaying the Client
We need some DOM elements to display the components. To keep the scope man-
ageable within the confines of a single chapter, we’re going to manually write the
required markup in the HTML file that serves the application.
The client is not going to make any sense to users without JavaScript, or without
a sufficiently capable JavaScript engine. To avoid presenting the user with controls
they cannot meaningfully use, we will initially hide all the chat related markup, and
have the individual controllers append the “js-chat” class name to the various ele-
ments used by the client. This way we can use CSS to display elements as JavaScript
enhances them.
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
392
TDD and DOM Manipulation: The Chat Client
15.2 The User Form
The user form is in charge of collecting the user’s desired chat name. As the server
currently has no concept of connected users, it does not need to validate the user
name in any way, i.e., two users may be online at the same time using the same
name. The controller requires a DOM form element as its view, and expects this to
contain at least one text input, from which it will read the username when the form
is submitted.
When the form is submitted, the controller will assign the user to a property of
the model object, to make it available to the rest of the application. Then it will emit
an event, allowing other parts of the application to act on the newly arrived user.
15.2.1 Setting the View
The first task is to set the view, i.e., assign the DOM element that is the visual
representation of the component.
15.2.1.1 Setting Up the Test Case

We start by setting up the test case and adding the first test, which expects user-
FormController to be an object. Listing 15.3 shows the initial test case. Save it
in test/user
_
form
_
controller
_
test.js.
Listing 15.3 Expecting the object to exist
(function () {
var userController = tddjs.chat.userFormController;
TestCase("UserFormControllerTest", {
"test should be object": function () {
assertObject(userController);
}
});
}());
Listing 15.4 passes the test by setting up the userFormController object.
Save the listing in src/user
_
form
_
controller.js.
Listing 15.4 Defining the controller
tddjs.namespace("chat").userFormController = {};
From the Library of WoweBook.Com
Download from www.eBookTM.com

×