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

Test Driven JavaScript Development- P19 ppsx

20 233 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 190,96 KB

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

Nội dung

*/ "should write status header": function test { var data = { data: { user: "cjno", message: "hi" } }; this.controller.post; this.sendRequestdata; test.okthis.res.writeHead.called; test.

Trang 1

Unfortunately, this doesn’t play out exactly as planned The previous test, which also calls post, is now attempting to call addMessage on chatRoom, which is undefinedin that test We can fix the issue by moving the chatRoom stub into setUpas Listing 14.18 does

Listing 14.18 Sharing the chatRoom stub

function controllerSetUp() { /* */

this.controller.chatRoom = { addMessage: stub() };

}

All the tests go back to a soothing green, and we can turn our attention to the duplicated logic we just introduced in the second test In particular, both tests simulates sending a request with a body We can simplify the tests considerably by extracting this logic into the setup Listing 14.19 shows the updated tests

Listing 14.19 Cleaning up post tests

function controllerSetUp() { /* */

this.sendRequest = function (data) { var str = encodeURI(JSON.stringify(data));

this.req.emit("data", str.substring(0, str.length / 2));

this.req.emit("data", str.substring(str.length / 2));

this.req.emit("end");

};

} testCase(exports, "chatRoomController.post", { /* */

"should parse request body as JSON": function (test) { var data = { data: { user: "cjno", message: "hi" } };

JSON.parse = stub(data);

this.controller.post();

this.sendRequest(data);

test.equals(JSON.parse.args[0], JSON.stringify(data));

test.done();

}, /* */

});

Trang 2

The cleaned up tests certainly are a lot easier to follow, and with the send-Requesthelper method, writing new tests that make requests will be easier as

well All tests pass and we can move on

14.2.4.3 Malicious Data

Notice that we are currently accepting messages completely unfiltered This can

lead to all kinds of scary situations, for instance consider the effects of the request

in Listing 14.20

Listing 14.20 Malicious request

{ "topic": "message",

"data": {

"user": "cjno",

"message":

"<script>window.location = 'http://hacked';</script>"

} }

Before deploying an application like the one we are currently building we should take care to not blindly accept any end user data unfiltered

14.2.5 Responding to Requests

When the controller has added the message, it should respond and close the

connec-tion In most web frameworks, output buffering and closing the connection happen

automatically behind the scenes The HTTP server support in Node, however, was

consciously designed with data streaming and long polling in mind For this reason,

data is never buffered, and connections are never closed until told to do so

http.ServerResponseobjects offer a few methods useful to output a re-sponse, namely writeHead, which writes the status code and response headers;

write, which writes a chunk to the response body; and finally end

14.2.5.1 Status Code

As there really isn’t much feedback to give the user when a message is added,

Listing 14.21 simply expects post to respond with an empty “201 Created.”

Listing 14.21 Expecting status code 201

function controllerSetUp() {

/* */

var res = this.res = { writeHead: stub() };

Trang 3

/* */

} testCase(exports, "chatRoomController.post", { /* */

"should write status header": function (test) { var data = { data: { user: "cjno", message: "hi" } };

this.controller.post();

this.sendRequest(data);

test.ok(this.res.writeHead.called);

test.equals(this.res.writeHead.args[0], 201);

test.done();

} });

Listing 14.22 faces the challenge and makes the actual call to writeHead

Listing 14.22 Setting the response code

post: function () { /* */

this.request.addListener("end", function () { var data = JSON.parse(decodeURI(body)).data;

this.chatRoom.addMessage(data.user, data.message);

this.response.writeHead(201);

}.bind(this));

}

14.2.5.2 Closing the Connection

Once the headers have been written, we should make sure the connection is closed

Listing 14.23 shows the test

Listing 14.23 Expecting the response to be closed

function controllerSetUp() { /* */

var res = this.res = { writeHead: stub(), end: stub()

};

/* */

};

Trang 4

testCase(exports, "chatRoomController.post", {

/* */

"should close connection": function (test) { var data = { data: { user: "cjno", message: "hi" } };

this.controller.post();

this.sendRequest(data);

test.ok(this.res.end.called);

test.done();

} });

The test fails, and Listing 14.24 shows the updated post method, which passes all the tests

Listing 14.24 Closing the response

post: function () {

/* */

this.request.addListener("end", function () { /* */

this.response.end();

}.bind(this));

}

That’s it for the post method It is now functional enough to properly handle well-formed requests In a real-world setting, however, I encourage more rigid input

verification and error handling Making the method more resilient is left as an

exercise

14.2.6 Taking the Application for a Spin

If we make a small adjustment to the server, we can now take the application for a

spin In the original listing, the server did not set up a chatRoom for the controller

To successfully run the application, update the server to match Listing 14.25

Listing 14.25 The final server

var http = require("http");

var url = require("url");

var crController = require("chapp/chat_room_controller");

var chatRoom = require("chapp/chat_room");

Trang 5

var room = Object.create(chatRoom);

module.exports = http.createServer(function (req, res) {

if (url.parse(req.url).pathname == "/comet") { var controller = crController.create(req, res);

controller.chatRoom = room;

controller[req.method.toLowerCase()]();

} });

For this to work, we need to add a fake chatRoom module Save the contents

of Listing 14.26 to lib/chapp/chat_room.js

Listing 14.26 A fake chat room

var sys = require("sys");

var chatRoom = { addMessage: function (user, message) { sys.puts(user + ": " + message);

} };

module.exports = chatRoom;

Listing 14.27 shows how to use node-repl, an interactive Node shell, to encode some POST data and post it to the application using curl, the command line HTTP client Run it in another shell, and watch the output from the shell that

is running the application

Listing 14.27 Manually testing the 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

When you enter that last command, you should get an immediate response (i.e.,

it simply returns to your prompt) and the shell that is running the server should

output “cjno: Enjoying Node.js.” In Chapter 15, TDD and DOM Manipulation: The

Chat Client, we will build a proper frontend for the application.

Trang 6

14.3 Domain Model and Storage

The domain model of the chat application will consist of a single chatRoom object

for the duration of our exercise chatRoom will simply store messages in memory,

but we will design it following Node’s I/O conventions

14.3.1 Creating a Chat Room

As with the controller, we will rely on Object.create to create new objects

inheriting from chatRoom However, until proved otherwise, chatRoom does

not need an initializer, so we can simply create objects with Object.create

directly Should we decide to add an initializer at a later point, we must update the

places that create chat room objects in the tests, which should be a good motivator

to keep from duplicating the call

14.3.2 I/O in Node

Because the chatRoom interface will take the role as the storage backend, we

classify it as an I/O interface This means it should follow Node’s carefully thought

out conventions for asynchronous I/O, even if it’s just an in-memory store for now

Doing so allows us to very easily refactor to use a persistence mechanism, such as a

database or web service, at a later point

In Node, asynchronous interfaces accept an optional callback as their last ar-gument The first argument passed to the callback is always either null or an error

object This removes the need for a dedicated “errback” function Listing 14.28

shows an example using the file system module

Listing 14.28 Callback and errback convention in Node

var fs = require("fs");

fs.rename("./tetx.txt", "./text.txt", function (err) {

if (err) { throw err;

} // Renamed successfully, carry on });

This convention is used for all low-level system interfaces, and it will be our starting point as well

Trang 7

14.3.3 Adding Messages

As dictated by the controller using it, the chatRoom object should have an ad-dMessagemethod that accepts a username and a message

14.3.3.1 Dealing with Bad Data

For basic data consistency, the addMessage method should err if either the user-name or message is missing However, as an asynchronous I/O interface, it cannot simply throw exceptions Rather, we will expect errors to be passed as the first ar-gument to the callback registered with addMessage, as is the Node way Listing 14.29 shows the test for missing username Save it in test/chapp/chat_room_

test.js

Listing 14.29 addMessage should require username

var testCase = require("nodeunit").testCase;

var chatRoom = require("chapp/chat_room");

testCase(exports, "chatRoom.addMessage", {

"should require username": function (test) { var room = Object.create(chatRoom);

room.addMessage(null, "a message", function (err) { test.isNotNull(err);

test.inherits(err, TypeError);

test.done();

});

} });

The test fails as expected, and so we add a check on the user parameter, as Listing 14.30 shows

Listing 14.30 Checking the username

var chatRoom = { addMessage: function (user, message, callback) {

if (!user) { callback(new TypeError("user is null"));

} } };

Trang 8

The test passes, and we can move on to checking the message The test in Listing 14.31 expects addMessage to require a message

Listing 14.31 addMessage should require message

"should require message": function (test) {

var room = Object.create(chatRoom);

room.addMessage("cjno", null, function (err) { test.isNotNull(err);

test.inherits(err, TypeError);

test.done();

});

}

The test introduces some duplication that we’ll deal with shortly First, Listing 14.32 makes the check that passes it

Listing 14.32 Checking the message

addMessage: function (user, message, callback) {

/* */

if (!message) { callback(new TypeError("message is null"));

} }

All the tests pass Listing 14.33 adds a setUp method to remove the duplicated creation of the chatRoom object

Listing 14.33 Adding a setUp method

testCase(exports, "chatRoom.addMessage", {

setUp: function () { this.room = Object.create(chatRoom);

}, /* */

});

As we decided previously, the callback should be optional, so Listing 14.34 adds

a test that expects the method not to fail when the callback is missing.

Trang 9

Listing 14.34 Expecting addMessage not to require a callback

/* */

require("function-bind");

/* */

testCase(exports, "chatRoom.addMessage", { /* */

"should not require a callback": function (test) { test.noException(function () {

this.room.addMessage();

test.done();

}.bind(this));

} }

Once again we load the custom bind implementation to bind the anonymous callback to test.noException To pass the test we need to check that the callback is callable before calling it, as Listing 14.35 shows

Listing 14.35 Verifying that callback is callable before calling it

addMessage: function (user, message, callback) { var err = null;

if (!user) { err = new TypeError("user is null"); }

if (!message) { err = new TypeError("message is null"); }

if (typeof callback == "function") { callback(err);

} }

14.3.3.2 Successfully Adding Messages

We won’t be able to verify that messages are actually stored until we have a way

to retrieve them, but we should get some indication on whether or not adding the message was successful To do this we’ll expect the method to call the callback with

a message object The object should contain the data we passed in along with an id

The test can be seen in Listing 14.36

Trang 10

Listing 14.36 Expecting addMessage to pass the created message

"should call callback with new object": function (test) {

var txt = "Some message";

this.room.addMessage("cjno", txt, function (err, msg) { test.isObject(msg);

test.isNumber(msg.id);

test.equals(msg.message, txt);

test.equals(msg.user, "cjno");

test.done();

});

}

Listing 14.37 shows an attempt at passing the test It calls the callback with an object and cheats the id by hard-coding it to 1

Listing 14.37 Passing the object to the callback

addMessage: function (user, message, callback) {

/* */

var data;

if (!err) { data = { id: 1, user: user, message: message };

}

if (typeof callback == "function") { callback(err, data);

} }

With this in place, the tests are back to green Next up, the id should be unique for every message Listing 14.38 shows the test

Listing 14.38 Expecting unique message ids

"should assign unique ids to messages": function (test) {

var user = "cjno";

this.room.addMessage(user, "a", function (err, msg1) { this.room.addMessage(user, "b", function (err, msg2) { test.notEquals(msg1.id, msg2.id);

test.done();

});

}.bind(this));

}

Trang 11

The test exposes our cheat, so we need to find a better way to generate ids

Listing 14.39 uses a simple variable that is incremented each time a message is added

Listing 14.39 Assigning unique integer ids

var id = 0;

var chatRoom = { addMessage: function (user, message, callback) { /* */

if (!err) { data = { id: id++, user: user, message: message };

} /* */

} };

Tests are passing again You might worry that we’re not actually storing the

message anywhere That is a problem, but it’s not currently being addressed by the

test case To do so we must start testing message retrieval

14.3.4 Fetching Messages

In the next chapter we will interface with the chat backend using the comet-Clientfrom Chapter 13, Streaming Data with Ajax and Comet This means that

chatRoomneeds some way to retrieve all messages since some token We’ll add a getMessagesSincemethod that accepts an id and yields an array of messages

to the callback

14.3.4.1 The getMessagesSince Method

The initial test for this method in Listing 14.40 adds two messages, then tries to retrieve all messages since the id of the first This way we don’t program any as-sumptions about how the ids are generated into the tests

Listing 14.40 Testing message retrieval

testCase(exports, "chatRoom.getMessagesSince", {

"should get messages since given id": function (test) { var room = Object.create(chatRoom);

var user = "cjno";

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