Phát triển Javascript - part 38 pps

10 192 0
Phát triển Javascript - part 38 pps

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

Thông tin tài liệu

ptg 14.1 The Node.js Runtime 343 I named the project “chapp,” as in “chat app.” The deps directory is for third party dependencies; the other two should be self-explanatory. 14.1.1.2 Testing Framework Node has a CommonJS compliant Assert module, but in line with the low-level focus of Node, it only provides a few assertions. No test runner, no test cases, and no high-level testing utilities; just the bare knuckles assertions, enabling framework authors to build their own. For this chapter we will be using a version of a small testing framework called Nodeunit. Nodeunit was originally designed to look like QUnit, jQuery’s unit testing framework. I have added some bells and whistles to it to bring it slightly closer to JsTestDriver in style, so testing with it should look familiar. The version of Nodeunit used for this chapter can be downloaded from the book’s website, 3 and should live in deps/nodeunit. Listing 14.2 shows a small script to help run tests. Save it in ./run _ tests and make it executable with chmod +x run _ tests. Listing 14.2 Script to run tests #!/usr/local/bin/node require.paths.push( __ dirname); require.paths.push( __ dirname + "/deps"); require.paths.push( __ dirname + "/lib"); require("nodeunit").testrunner.run(["test/chapp"]); 14.1.2 Starting Point There’s a lot of code ahead of us, and to get us started I will provide a basic starting point, consisting of a small HTTP server and a convenient script to start it. We will then proceed top-down, actually taking the server for a spin halfway. 14.1.2.1 The Server To create an HTTP server in Node we need the http module and its create- Server method. This method accepts a function, which will be attached as a request listener. CommonJS modules will be properly introduced in a moment, 3. http://tddjs.com From the Library of WoweBook.Com Download from www.eBookTM.com ptg 344 Server-Side JavaScript with Node.js as will Node’s event module. Listing 14.3 shows the server, which should live in lib/chapp/server.js. Listing 14.3 A Node.js HTTP server var http = require("http"); var url = require("url"); var crController = require("chapp/chat_room_controller"); module.exports = http.createServer(function (req, res) { if (url.parse(req.url).pathname == "/comet") { var controller = crController.create(req, res); controller[req.method.toLowerCase()](); } }); The server requires the first module that we are going to write—the chat- RoomController, which deals with the request/response logic. The server cur- rently only responds to requests to the /comet URL. 14.1.2.2 The Startup Script To start the server we need a script similar to the run _ tests script, which sets up the load path, requires the server file, and starts the server. Listing 14.4 shows the script, which should be saved in ./run _ server, and should be made executable with chmod +x run _ server. Listing 14.4 Startup script #!/usr/local/bin/node require.paths.push( __ dirname); require.paths.push( __ dirname + "/deps"); require.paths.push( __ dirname + "/lib"); require("chapp/server").listen(process.argv[2] || 8000); The listen call starts the server. process.argv contains all the command line arguments, i.e., the interpreter, the file being run, and any additional arguments given when running the script. The script is run with ./run _ server 8080. Leaving out the port number starts the server on the default port 8000. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.2 The Controller 345 14.2 The Controller For any request to the /comet URL, the server will call the controller’s create method, passing it request and response objects. It then proceeds to call a method on the resulting controller corresponding to the HTTP method used. In this chapter we will only implement the get and post methods. 14.2.1 CommonJS Modules Node implements CommonJS modules, a structured way to manage reusable JavaScript components. Unlike script files loaded in browsers, the implicit scope in modules is not the global scope. This means that we don’t need to wrap everything in anonymous closures to avoid leaking identifiers. To add a function or object to the module, we assign properties on the special exports object. Alternatively, we can specify the entire module as a single object, and assign this to module. exports = myModule. Modules are loaded with require("my _ module"). This function uses the paths specified in the require.paths array, which can be modified as we see fit, just like we did in Listing 14.2. We can also load modules not on the load path by prefixing the module name with "./", which causes Node to look for the module relative to the current module file. 14.2.2 Defining the Module: The First Test With a basic overview of CommonJS modules, we can write our very first test, as seen in Listing 14.5. It asserts that the controller object exists, and that it has a create method. Listing 14.5 Expecting the controller to exist var testCase = require("nodeunit").testCase; var chatRoomController = require("chapp/chat_room_controller"); testCase(exports, "chatRoomController", { "should be object": function (test) { test.isNotNull(chatRoomController); test.isFunction(chatRoomController.create); test.done(); } }); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 346 Server-Side JavaScript with Node.js Save the test in test/chapp/chat _ room _ controller _ test.js and run it with ./run _ tests. It fails horribly with an exception stating that Node “Can’t find module chapp/chat room controller.” Save the contents of Listing 14.6 in lib/chapp/chat _ room _ controller.js to resolve the issue. Listing 14.6 Creating the controller module var chatRoomController = { create: function () {} }; module.exports = chatRoomController; Running the tests again should produce more uplifting output along the lines of Listing 14.7. Listing 14.7 First successful test chris@laptop:~/projects/chapp$ ./run_tests test/chapp/chat_room_controller_test.js chatRoomController should be object OK: 2 assertions (2ms) Note how the test case receives a test object and calls its done method. Nodeunit runs tests asynchronously, so we need to let it know explicitly when a test is done. In Part I, Test-Driven Development, I argued that unit tests rarely need to be asynchronous. For Node the situation is a little bit different, because not allowing asynchronous tests would basically mean having to stub or mock every system call, which simply is not a viable option. Doing so would make testing challenging, and without proper interface enforcement, error-prone. 14.2.3 Creating a Controller Listing 14.8 creates a controller and asserts that it has request and response properties corresponding to the arguments we pass the create method. Listing 14.8 Test creating new controllers testCase(exports, "chatRoomController.create", { "should return object with request and response": function (test) { var req = {}; var res = {}; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.2 The Controller 347 var controller = chatRoomController.create(req, res); test.inherits(controller, chatRoomController); test.strictEqual(controller.request, req); test.strictEqual(controller.response, res); test.done(); } }); Notice that Node’s assertions flip the order of the arguments compared with what we’re used to with JsTestDriver. Here, the order is actual, expected rather than the usual expected, actual. This is an important detail to get right, as failure messages will suffer if we don’t. As V8 implements parts of ECMAScript5, we can pass this test by using Object.create, as Listing 14.9 shows. Listing 14.9 Creating controllers var chatRoomController = { create: function (request, response) { return Object.create(this, { request: { value: request }, response: { value: response } }); } }; The test passes. Defining request and response this way means that their enumerable, configurable and writable attributes are set to the default value, which in all cases is false. But you don’t need to trust me, you can test itusing test.isWritable, test.isConfigurable and test.isEnumerable, or their counterparts, test.isNot*. 14.2.4 Adding Messages on POST The post action accepts JSON in the format sent by cometClient from Chapter 13, Streaming Data with Ajax and Comet, and creates messages. If your memory’s a bit rusty on the JSON format, a sample request to create a message can be seen in Listing 14.10. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 348 Server-Side JavaScript with Node.js Listing 14.10 JSON request to create message { "topic": "message", "data": { "user": "cjno", "message": "Listening to the new 1349 album" } } The outer “topic” property describes what kind of event to create, in this example a new message, whereas the outer “data” property holds the actual data. The client was made this way so it could post different types of client-side events to the same server resource. For instance, when someone joins the chat, the client might send JSON like Listing 14.11. Listing 14.11 JSON request to join the chat room { "topic": "userEnter", "data": { "user": "cjno" } } If the backend is ever extended to support several chat rooms, the message might also include which room the user entered. 14.2.4.1 Reading the Request Body The first thing post needs to do is retrieve the request body, which contains the URL encoded JSON string. As a request comes in, the request object will emit “data” events, passing chunks of the request body. When all chunks have arrived, the request object emits a “end” event. The equivalent of our observ- able from Chapter 11, The Observer Pattern, that powers Node’s events is the events.EventEmitter interface. In tests, we will stub the request object, which needs to be an EventEmit- ter so we can trigger the “data” and “end” events we are interested in testing. We can then emit a couple of chunks from the test, and assert that the joined string is passed to JSON.parse. To verify that the entire body is passed to JSON.parse, we can stub it using the stub function from Chapter 12, Abstracting Browser Differ- ences: Ajax. Save Listing 14.12 in deps/stub.js. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.2 The Controller 349 Listing 14.12 Using stubFn with Node module.exports = function (returnValue) { function stub() { stub.called = true; stub.args = arguments; stub.thisArg = this; return returnValue; } stub.called = false; return stub; }; Listing 14.13 shows the test. It includes quite a bit of setup code, which we will move around in a moment. Listing 14.13 Expecting the request body to be parsed as JSON var EventEmitter = require("events").EventEmitter; var stub = require("stub"); /* */ testCase(exports, "chatRoomController.post", { setUp: function () { this.jsonParse = JSON.parse; }, tearDown: function () { JSON.parse = this.jsonParse; }, "should parse request body as JSON": function (test) { var req = new EventEmitter(); var controller = chatRoomController.create(req, {}); var data = { data: { user: "cjno", message: "hi" } }; var stringData = JSON.stringify(data); var str = encodeURI(stringData); JSON.parse = stub(data); controller.post(); req.emit("data", str.substring(0, str.length / 2)); req.emit("data", str.substring(str.length / 2)); req.emit("end"); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 350 Server-Side JavaScript with Node.js test.equals(JSON.parse.args[0], stringData); test.done(); } }); setUp and tearDown take care of restoring JSON.parse after the test has stubbed it out. We then create a controller object using fake request and response objects along with some test data to POST. Because the tddjs.ajax tools built in the two previous chapters currently only support URL encoded data, we must encode the test data to fit. The test then emits a simple URL encoded JSON string in two chunks, the “end” event, and finally expects the JSON.parse method to have been called. Phew! Listing 14.14 shows one way to pass the test. Listing 14.14 Reading the request body and parsing it as JSON var chatRoomController = { /* */ post: function () { var body = ""; this.request.addListener("data", function (chunk) { body += chunk; }); this.request.addListener("end", function () { JSON.parse(decodeURI(body)); }); } }; As the test passes it is time to remove duplication. Aggressively removing dupli- cation is the key to a flexible code base that is easy to change and mold any way we see fit. The tests are part of code base, and need constant refactoring and improve- ment too. Both the test cases for create and post create a controller instance using stub request and response objects, and sure enough, the get test case will do just the same. We can extract this into a function that can be used as a shared setup method. Listing 14.15 has the lowdown. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.2 The Controller 351 Listing 14.15 Sharing setup function controllerSetUp() { var req = this.req = new EventEmitter(); var res = this.res = {}; this.controller = chatRoomController.create(req, res); this.jsonParse = JSON.parse; } function controllerTearDown() { JSON.parse = this.jsonParse; } /* */ testCase(exports, "chatRoomController.create", { setUp: controllerSetUp, /* */ }); testCase(exports, "chatRoomController.post", { setUp: controllerSetUp, tearDown: controllerTearDown, /* */ }); With this change the tests should refer to controller, req and res as properties of this. 14.2.4.2 Extracting the Message With the request body readily parsed as JSON, we need to extract the message from the resulting object and pass it somewhere it will be kept safe. As we’re going through this exercise top-down, we don’t have a data model yet. We will have to decide roughly what it’s going to look like, and stub it while we finish the post method. Messages should belong to a chat room. As the chat room needs to persist between requests, the controller will depend on the server assigning it a chatRoom object, on which it can call addMessage(user, message). The test in Listing 14.16 verifies that post passes data to addMessage according to this interface. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 352 Server-Side JavaScript with Node.js Listing 14.16 Expecting post to add message "should add message from request body": function (test) { var data = { data: { user: "cjno", message: "hi" } }; this.controller.chatRoom = { addMessage: stub() }; this.controller.post(); this.req.emit("data", encodeURI(JSON.stringify(data))); this.req.emit("end"); test.ok(this.controller.chatRoom.addMessage.called); var args = this.controller.chatRoom.addMessage.args; test.equals(args[0], data.data.user); test.equals(args[1], data.data.message); test.done(); } As before, we call the post method to have it add its request body listeners, then we emit some fake request data. Finally we expect the controller to have called chatRoom.addMessage with the correct arguments. To pass this test we need to access this.chatRoom from inside the anony- mous “end” event handler. To achieve this we can bind it to avoid having to manu- ally keep local references to this. At the time of writing, V8 does not yet support Function.prototype.bind, but we can use the custom implementation from Listing 6.7 in Chapter 6, Applied Functions and Closures. Save the implementation in deps/function-bind.js and Listing 14.17 should run as expected. Listing 14.17 Adding messages on POST require("function-bind"); var chatRoomController = { /* */ post: function () { /* */ this.request.addListener("end", function () { var data = JSON.parse(decodeURI(body)).data; this.chatRoom.addMessage(data.user, data.message); }.bind(this)); } }; From the Library of WoweBook.Com Download from www.eBookTM.com . directory is for third party dependencies; the other two should be self-explanatory. 14.1.1.2 Testing Framework Node has a CommonJS compliant Assert module, but in line with the low-level focus of Node,. removing dupli- cation is the key to a flexible code base that is easy to change and mold any way we see fit. The tests are part of code base, and need constant refactoring and improve- ment too http://tddjs.com From the Library of WoweBook.Com Download from www.eBookTM.com ptg 344 Server-Side JavaScript with Node.js as will Node’s event module. Listing 14.3 shows the server, which should

Ngày đăng: 04/07/2014, 22:20

Tài liệu cùng người dùng

Tài liệu liên quan