Permalink
Browse files

Add post "Testing Private State and Mocking Dependencies"

  • Loading branch information...
1 parent 18f598a commit ad3baaf147b54febcd9f5cb1a1ab367a0838c8c6 @vojtajina vojtajina committed Jan 8, 2012
@@ -0,0 +1,120 @@
+Title: Testing Private State and Mocking Dependencies
+Author: Vojta Jina
+Date: Sun Jan 08 2012 00:11:06 GMT-0800 (PST)
+Node: v0.6.5
+
+
+During Christmas I've been working on [SlimJim] and found some tricks how to make my testing life
+easier. It's nothing special at all, just a simple way **how to access private state of a module**
+and **how to mock out some dependencies**. I've found these two techniques pretty usefull, so I
+believe it might help someone else as well...
+
+
+
+### Why would you need to access private state of a module?
+
+Private should be private, right? Yes, for sure. But during unit tests, it can be very helpful to
+have access to private state of a module - I always try to cover functionality or bug at the lowest
+possible level, because it's simply cheaper:
+
+- faster test execution
+- less code is required to bootstrap the test
+
+Let's say we are building very simple static web server, the skeleton might look something like
+this:
+
+ var http = require('http');
+
+ var handleRequest = function(request, response) {
+ // read file from fs and send response
+ };
+
+ exports.createServer = function() {
+ return http.createServer(handleRequest);
+ };
+
+This module has only one public method `createServer`, so unless we make it public, we can't get
+hold of anything else but this method. That sucks, because `HttpServer` doesn't have any public
+method to call the handler, so we would have to send some data through socket to test it. That's way
+too much effort, especially when you realize that the only code we really need to test is the
+`handleRequest` function - everything else is just Node and we trust Node, because it's awesome. We
+need to test **our** code - that's where all the bugs are.
+
+
+
+### Why would you need to mock out dependencies?
+
+Some dependencies are cheap, some not. When our code uses modules like `util` or `path`, we are
+fine. Nothing bad happens there. But when it comes to some other modules like `fs`, `net` or `http`,
+it's totally different story. We simply don't want to deal with real filesystem in unit tests. There
+are many reasons for that, such as:
+
+- accessing file system is slow
+- it requires seting up some state of filesystem
+- there is only one instance of filesystem, so conflicts between different unit tests might happen
+
+So we want our module to use something different - we call these objects test doubles (I actually
+like using mock/stub/dummy definition from [G.Meszaros]). The question is, how can we persuade our
+awesome module, to use **a different instance during testing and different instance in production**?
+
+**Dependency Injection** is great for this - it wires all the pieces together (yep, it saves us lot
+of work) - and more than that, it does allow us to use different instances during testing. Yep, DI is
+just awesome! I actually think, that new languages such as [Dart] should support DI natively - in the
+same way as they do support memory management.
+
+Unfortunately, there is no DI in Node, at least I haven't found any sufficient implementation.
+Writing a Dependency Injection framework is definitely a solution, but I was looking for something
+faster...
+
+
+
+## Let's do it !
+
+
+### Module Loader
+
+<testing-private-state-and-mocking-deps/module-loader.js>
+
+**This is actually the code this post is all about :-D**
+
+Instead of using Node's `require`, we use `loadModule` function, which reads the content of
+requested module (javascript source file) and executes it on the `context` object. So all the
+private state of the module is dumped into the `context` object and yay, we can access everything!
+See [vm.runInNewContext] for more info.
+
+Inside this `context` object, we defined our own `require` function, which means whenever the module
+asks for a dependency, our `loadModule` will be called intead of Node's `require`. That's pretty
+cool, because **we can decide, whether we want to return a mock or real module**, in which case we
+delegate the request to Node's `require`.
+
+
+### Very simple web server example
+
+<testing-private-state-and-mocking-deps/web-server.js>
+
+
+### Let's use it in test now
+
+<testing-private-state-and-mocking-deps/web-server.test.js>
+
+This is very simple example of unit testing `web-server` module, using `loadModule` function.
+
+We can access both private functions as properties of `module` now, which is great, because we can
+add more tests very easily. For example, you might have noticed, that `extensionFromUrl` won't
+return correct extension when requested url contains query param. Piece of cake, just add a test
+that covers this bug:
+
+ it('extensionFromUrl() should ignore query params', function() {
+ expect(module.extensionFromUrl('/some.html?param=ignored')).toBe('html');
+ });
+
+The second test only asserts whether we set proper status code for existing file. We should assert
+status code for non existing file as well as caching headers, content type header and many other
+stuff. The important point here is, that **it's fast, because it doesn't touch the real filesystem
+and still does test what needs to be tested - our code**.
+
+
+[SlimJim]: http://github.com/vojtajina/slim-jim/
+[G.Meszaros]: http://xunitpatterns.com/Test%20Double.html
+[vm.runInNewContext]: http://nodejs.org/docs/latest/api/vm.html#vm.runInNewContext
+[Dart]: http://www.dartlang.org/
@@ -0,0 +1,38 @@
+var vm = require('vm');
+var fs = require('fs');
+var path = require('path');
+
+/**
+ * Helper for unit testing:
+ * - load module with mocked dependencies
+ * - allow accessing private state of the module
+ *
+ * @param {string} filePath Absolute path to module (file to load)
+ * @param {Object=} mocks Hash of mocked dependencies
+ */
+exports.loadModule = function(filePath, mocks) {
+ mocks = mocks || {};
+
+ // this is necessary to allow relative path modules within loaded file
+ // i.e. requiring ./some inside file /a/b.js needs to be resolved to /a/some
+ var resolveModule = function(module) {
+ if (module.charAt(0) !== '.') return module;
+ return path.resolve(path.dirname(filePath), module);
+ };
+
+ var exports = {};
+ var context = {
+ require: function(name) {
+ return mocks[name] || require(resolveModule(name));
+ },
+ console: console,
+ exports: exports,
+ module: {
+ exports: exports
+ }
+ };
+
+ vm.runInNewContext(fs.readFileSync(filePath), context);
+ return context;
+};
+
@@ -0,0 +1,28 @@
+// this returns either real fs or mock fs (in test)
+var fs = require('fs');
+var http = require('http');
+
+var MIME_TYPE = {
+ txt: 'text/plain',
+ html: 'text/html',
+ js: 'application/javascript'
+};
+
+// we can access and test this function directly, without instantiating anything
+var extensionFromUrl = function(url) {
+ return url.split('.').pop(); //.replace(/\?.*$/, '');
+};
+
+var handleRequest = function(request, response) {
+ fs.readFile(__dirname + request.url, function(error, data) {
+ var contentType = MIME_TYPE[extensionFromUrl(request.url)] || MIME_TYPE.txt;
+ response.setHeader('Content-Type', contentType);
+ response.writeHead(error ? 404 : 200);
+ response.end(data);
+ });
+};
+
+// the public method, the only visible property of the module
+exports.createServer = function() {
+ return http.createServer(handleRequest);
+};
@@ -0,0 +1,37 @@
+// Jasmine's syntax http://pivotal.github.com/jasmine/
+describe('web-server', function() {
+ // assuming we have mocks module :-D
+ var mocks = require('./mocks');
+ var loadModule = require('module-loader').loadModule;
+ var module, fsMock, mockRequest, mockResponse;
+
+ beforeEach(function() {
+ fsMock = mocks.createFs();
+ mockRequest = mocks.createRequest();
+ mockResponse = mocks.createResponse();
+
+ // load the module with mock fs instead of real fs
+ // publish all the private state as an object
+ module = loadModule('./web-server.js', {fs: fsMock});
+ });
+
+ it('extensionFromUrl() should parse basic extensions', function() {
+ // we are testing private method of the module directly
+ expect(module.extensionFromUrl('/some.html')).toBe('html');
+ expect(module.extensionFromUrl('/another/file.js')).toBe('js');
+ expect(module.extensionFromUrl('/file.with.more.dots.js')).toBe('js');
+ });
+
+ it('should return 200 if file exists', function() {
+ // tell the mock, that this file exists
+ fsMock.file('some/file.html');
+
+ module.handleRequest(mockRequest, mockResponse);
+
+ // wait for finishing the response, it's async
+ waitsFor(function() {mockResponse.isFinished();});
+ runs(function() {
+ expect(mockResponse.status).toBe(200);
+ });
+ });
+});
@@ -0,0 +1,7 @@
+Github: vojtajina
+Email: vojta.jina@gmail.com
+Twitter: vojtajina
+Location: San Mateo, CA
+
+Trying to make development of web apps easier and more fun - working on AngularJS at Google, MTV.
+Trumpet player and fast driver in free time :-D

0 comments on commit ad3baaf

Please sign in to comment.