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

Test Driven JavaScript Development- P20 ppsx

20 266 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 197,48 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 14.54 Expecting chatRoom to be event emitter testCaseexports, "chatRoom", { "should be event emitter": function test { test.isFunctionchatRoom.addListener; test.isFunctionchatRoo

Trang 1

Listing 14.54 Expecting chatRoom to be event emitter

testCase(exports, "chatRoom", {

"should be event emitter": function (test) { test.isFunction(chatRoom.addListener);

test.isFunction(chatRoom.emit);

test.done();

} });

We can pass this test by popping EventEmitter.prototype in as chat-Room’s prototype, as seen in Listing 14.55

Listing 14.55 chatRoom inheriting from EventEmitter.prototype

/* */

var EventEmitter = require("events").EventEmitter;

/* */

var chatRoom = Object.create(EventEmitter.prototype);

chatRoom.addMessage = function (user, message) {/* */};

chatRoom.getMessagesSince = function (id) {/* */};

Note that because V8 fully supports ECMAScript 5’s Object.create, we could have used property descriptors to add the methods as well, as seen in Listing 14.56

Listing 14.56 chatRoom defined with property descriptors

var chatRoom = Object.create(EventEmitter.prototype, { addMessage: {

value: function (user, message) { /* */

} }, getMessagesSince: { value: function (id) { /* */

} } });

Trang 2

At this point the property descriptors don’t provide anything we have a doc-umented need for (i.e., the ability to override default property attribute values),

so we’ll avoid the added indentation and stick with the simple assignments in

Listing 14.55

Next up, we make sure that addMessage emits an event Listing 14.57 shows the test

Listing 14.57 Expecting addMessage to emit a “message” event

testCase(exports, "chatRoom.addMessage", {

/* */

"should emit 'message' event": function (test) { var message;

this.room.addListener("message", function (m) { message = m;

});

this.room.addMessage("cjno", "msg").then(function (m) { test.same(m, message);

test.done();

});

} });

To pass this test we need to place a call to emit right before we resolve the promise, as seen in Listing 14.58

Listing 14.58 Emitting a message event

chatRoom.addMessage= function (user, message, callback) {

var promise = new Promise() process.nextTick(function () { /* */

if (!err) { /* */

this.emit("message", data);

promise.resolve(data);

} else { promise.reject(err, true);

}

Trang 3

}.bind(this));

return promise;

};

With the event in place, we can build the waitForMessagesSince method

14.5.2 Waiting for Messages

The waitForMessagesSince method will do one of two things; if messages are available since the provided id, the returned promise will resolve immediately If no messages are currently available, the method will add a listener for the “message”

event, and the returned promise will resolve once a new message is added

The test in Listing 14.59 expects that the promise is immediately resolved if messages are available

Listing 14.59 Expecting available messages to resolve immediately

/* */

var Promise = require("node-promise/promise").Promise;

var stub = require("stub");

/* */

testCase(exports, "chatRoom.waitForMessagesSince", { setUp: chatRoomSetup,

"should yield existing messages": function (test) { var promise = new Promise();

promise.resolve([{ id: 43 }]);

this.room.getMessagesSince = stub(promise);

this.room.waitForMessagesSince(42).then(function (m) { test.same([{ id: 43 }], m);

test.done();

});

} });

This test stubs the getMessagesSince method to verify that its results are used if there are any To pass this test we can simply return the promise returned from getMessagesSince, as seen in Listing 14.60

Trang 4

Listing 14.60 Proxying getMessagesSince

chatRoom.waitForMessagesSince = function (id) {

return this.getMessagesSince(id);

};

Now to the interesting part If the attempt to fetch existing methods does not succeed, the method should add a listener for the “message” event and go to sleep

Listing 14.61 tests this by stubbing addListener

Listing 14.61 Expecting the wait method to add a listener

"should add listener when no messages": function (test) {

this.room.addListener = stub();

var promise = new Promise();

promise.resolve([]);

this.room.getMessagesSince = stub(promise);

this.room.waitForMessagesSince(0);

process.nextTick(function () { test.equals(this.room.addListener.args[0], "message");

test.isFunction(this.room.addListener.args[1]);

test.done();

}.bind(this));

}

Again we stub the getMessagesSince method to control its output We then resolve the promise it’s stubbed to return, passing an empty array This

should cause the waitForMessagesSince method to register a listener for

the “message” event Seeing as waitForMessagesSince does not add a

lis-tener, the test fails To pass it, we need to change the implementation as seen in

Listing 14.62

Listing 14.62 Adding a listener if no messages are available

chatRoom.waitForMessagesSince = function (id) {

var promise = new Promise();

this.getMessagesSince(id).then(function (messages) {

if (messages.length > 0) { promise.resolve(messages);

} else { this.addListener("message", function () {});

}

Trang 5

}.bind(this));

return promise;

};

The listener we just added is empty, as we don’t yet have a test that tells us what

it needs to do That seems like a suitable topic for the next test, which will assert that adding a message causes waitForMessagesSince to resolve with the new message For symmetry with getMessagesSince, we expect the single message

to arrive as an array Listing 14.63 shows the test

Listing 14.63 Adding a message should resolve waiting requests

"new message should resolve waiting": function (test) { var user = "cjno";

var msg = "Are you waiting for this?";

this.room.waitForMessagesSince(0).then(function (msgs) { test.isArray(msgs);

test.equals(msgs.length, 1);

test.equals(msgs[0].user, user);

test.equals(msgs[0].message, msg);

test.done();

});

process.nextTick(function () { this.room.addMessage(user, msg);

}.bind(this));

}

Unsurprisingly, the test does not pass, prompting us to fill in the “message”

listener we just added Listing 14.64 shows the working listener

Listing 14.64 Implementing the message listener

/* */

this.addListener("message", function (message) { promise.resolve([message]);

});

/* */

And that’s all it takes, the tests all pass, and our very rudimentary data layer is complete enough to serve its purpose in the application Still, there is one very im-portant task to complete, and one that I will leave as an exercise Once the promise

Trang 6

returned from waitForMessagesSince is resolved, the listener added to the

“message” event needs to be cleared Otherwise, the original call to

waitForMes-sagesSincewill have its callback called every time a message is added, even after

the current request has ended

To do this you will need a reference to the function added as a handler, and use this.removeListener To test it, it will be helpful to know that

room.listeners()returns the array of listeners, for your inspection pleasure

14.6 Returning to the Controller

With a functional data layer we can get back to finishing the controller We’re going

to give post the final polish and implement get

14.6.1 Finishing the post Method

The post method currently responds with the 201 status code, regardless of

whether the message was added or not, which is in violation with the

seman-tics of a 201 response; the HTTP spec states that “The origin server MUST

cre-ate the resource before returning the 201 status code.” Having implemented the

addMessagemethod we know that this is not necessarily the case in our current

implementation Let’s get right on fixing that

The test that expects post to call writeHead needs updating We now expect the headers to be written once the addMessage method resolves Listing 14.65

shows the updated test

Listing 14.65 Expecting post to respond immediately when addMessage

resolves

/* */

var Promise = require("node-promise/promise").Promise;

/* */

function controllerSetUp() {

/* */

var promise = this.addMessagePromise = new Promise();

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

/* */

}

/* */

testCase(exports, "chatRoomController.post", {

Trang 7

/* */

"should write status header when addMessage resolves":

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

this.controller.post();

this.sendRequest(data);

this.addMessagePromise.resolve({});

process.nextTick(function () { test.ok(this.res.writeHead.called);

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

test.done();

}.bind(this));

}, /* */

});

Delaying the verification doesn’t affect the test very much, so the fact that

it still passes only tells us none of the new setup code is broken We can apply the same update to the following test, which expects the connection to be closed

Listing 14.66 shows the updated test

Listing 14.66 Expecting post not to close connection immediately

"should close connection when addMessage resolves":

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

this.controller.post();

this.sendRequest(data);

this.addMessagePromise.resolve({});

process.nextTick(function () { test.ok(this.res.end.called);

test.done();

}.bind(this));

}

Listing 14.67 shows a new test, which contradicts the two tests the way they

were previously written This test specifically expects the action not to respond

before addMessage has resolved

Trang 8

Listing 14.67 Expecting post not to respond immediately

"should not respond immediately": function (test) {

this.controller.post();

this.sendRequest({ data: {} });

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

test.done();

}

This test does not run as smoothly as the previous two Passing it is a matter

of deferring the closing calls until the promise returned by addMessage resolves

Listing 14.68 has the lowdown

Listing 14.68 post responds when addMessage resolves

post: function () {

/* */

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

this.chatRoom.addMessage(

data.user, data.message ).then(function () { this.response.writeHead(201);

this.response.end();

}.bind(this));

}.bind(this));

}

That’s about it for the post method Note that the method does not handle errors in any way; in fact it will respond with a 201 status even if the message was

not added successfully I’ll leave fixing it as an exercise

14.6.2 Streaming Messages with GET

GET requests should either be immediately responded to with messages, or held

open until messages are available Luckily, we did most of the heavy lifting while

implementing chatRoom.waitForMessagesSince, so the get method of the

controller will simply glue together the request and the data

Trang 9

14.6.2.1 Filtering Messages with Access Tokens

Remember how the cometClient from Chapter 13, Streaming Data with Ajax

and Comet, informs the server of what data to retrieve? We set it up to use the

X-Access-Tokenheader, which can contain any value and is controlled by the server Because we built waitForMessagesSince to use ids, it should not come

as a surprise that we are going to track progress using them

When a client connects for the first time, it’s going to send an empty X-Access-Token, so handling that case seems like a good start Listing 14.69 shows the test for the initial attempt We expect the controller to simply return all available messages on first attempt, meaning that empty access token should imply waiting for messages since 0

Listing 14.69 Expecting the client to grab all messages

testCase(exports, "chatRoomController.get", { setUp: controllerSetUp,

tearDown: controllerTearDown,

"should wait for any message": function (test) { this.req.headers = { "x-access-token": "" };

var chatRoom = this.controller.chatRoom;

chatRoom.waitForMessagesSince = stub();

this.controller.get();

test.ok(chatRoom.waitForMessagesSince.called);

test.equals(chatRoom.waitForMessagesSince.args[0], 0);

test.done();

} });

Notice that Node downcases the headers Failing to recognize this may take away some precious minutes from your life Or so I’ve heard To pass this test we can cheat by passing the expected id directly to the method, as Listing 14.70 does

Listing 14.70 Cheating to pass tests

var chatRoomController = { /* */

get: function () { this.chatRoom.waitForMessagesSince(0);

} };

Trang 10

The test passes Onward to the subsequent requests, which should be coming

in with an access token Listing 14.71 stubs the access token with an actual value,

and expects this to be passed to waitForMessagesSince

Listing 14.71 Expecting get to pass the access token

"should wait for messages since X-Access-Token":

function (test) {

this.req.headers = { "x-access-token": "2" };

var chatRoom = this.controller.chatRoom;

chatRoom.waitForMessagesSince = stub();

this.controller.get();

test.ok(chatRoom.waitForMessagesSince.called);

test.equals(chatRoom.waitForMessagesSince.args[0], 2);

test.done();

}

This test looks a lot like the previous one, only it expects the passed id to be the same as provided with the X-Access-Token header These tests could need

some cleaning up, and I encourage you to give them a spin Passing the test is simple,

as Listing 14.72 shows

Listing 14.72 Passing the access token header

get: function () {

var id = this.request.headers["x-access-token"] || 0;

this.chatRoom.waitForMessagesSince(id);

}

14.6.2.2 The respond Method

Along with the response body, which should be a JSON response of some kind, the

getmethod should also send status code and possibly some response headers, and

finally close the connection This sounds awfully similar to what post is currently

doing We’ll extract the response into a new method in order to reuse it with the get

request Listing 14.73 shows two test cases for it, copied from the post test case

Listing 14.73 Initial tests for respond

testCase(exports, "chatRoomController.respond", {

setUp: controllerSetUp,

"should write status code": function (test) { this.controller.respond(201);

Trang 11

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

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