-
In this tutorial we'll create a simplistic Logger class to demonstrate how to use TinyMock. We'll write code in coffeescript, run everything on Node.js, and use mocha and chai for our testing framework.
Ensure that you have Node.js installed before proceeding; mocha, chai, coffeescript, and TinyMock will be installed in the steps below.
All of the files for this tutorial can be found here.
-
Create a project directory, and create subdirectories for source code and test code files:
$ mkdir -p ~/tutorial/src ~/tutorial/test $ cd ~/tutorial
-
Launch an editor and create a minimal Cakefile that uses mocha to run our tests:
{exec} = require 'child_process' task 'test', 'Run unit tests', -> exec 'mocha --compilers coffee:coffee-script --require coffee-script --reporter spec --colors', (err, stdout, stderr) -> throw err if err console.log stdout + stderr
-
Also create a minimal package.json ():
{ "name": "TinyMock.tutorial", "description": "A TinyMock tutorial.", "repository": "git://github.com/milewdev/TinyMock.doc.git", "scripts": { "test": "cake test" }, "devDependencies": { "mocha": "*", "chai": "*", "coffee-script": "~1.6", "TinyMock": "https://github.com/milewdev/TinyMock/raw/master/TinyMock-0.4.0.tgz" } }
-
Use npm to install mocha, chai, the coffee-script compiler, and TinyMock:
$ npm install npm WARN package.json TinyMock.tutorial@ No README data npm http GET https://registry.npmjs.org/coffee-script npm http GET https://github.com/milewdev/TinyMock/raw/master/TinyMock-0.4.0.tgz ... npm http 200 https://registry.npmjs.org/mkdirp/-/mkdirp-0.4.0.tgz npm http 304 https://registry.npmjs.org/lru-cache TinyMock@0.4.0 node_modules/TinyMock coffee-script@1.6.3 node_modules/coffee-script chai@1.9.1 node_modules/chai ├── assertion-error@1.0.0 └── deep-eql@0.1.3 (type-detect@0.1.1) mocha@1.18.2 node_modules/mocha ├── debug@0.8.0 ├── diff@1.0.7 ├── growl@1.7.0 ├── commander@2.0.0 ├── mkdirp@0.3.5 ├── jade@0.26.3 (commander@0.6.1, mkdirp@0.4.0) └── glob@3.2.3 (inherits@2.0.1, graceful-fs@2.0.3, minimatch@0.2.14) $
Ignore the warning on the first line about 'No README data'; this is just a tutorial so we didn't create a readme file. -
Create the test code file test/Logger.test.coffee; start by requiring chai, TinyMock,
and our yet-to-be-written Logger class ():
chai = require("chai") should = chai.should() mock = require("TinyMock") Logger = require("../src/Logger")
-
Add a test for Logger's constructor to test/Logger.test.coffee ():
chai = require("chai") should = chai.should() mock = require("TinyMock") Logger = require("../src/Logger")
mock() takes a function argument; it creates five mock objects and invokes the function passing it the five mocks which the function can choose to use as it sees fit. In our code above, we use just one of the mocks which we reference with the variable 'options'. Here's how you might use all five mocks:describe "Logger", -> describe "constructor(options)", -> it "retrieves the log file name from 'options'", -> mock (options) -> options.expects("get_log_filename").returns("my_filename.log") logger = new Logger(options) logger.log_filename.should.equal("my_filename.log")
mock (store, inventory, book1, book2, customer) -> store.expects("get_inventory").returns(inventory) inventory.expects("get_books").returns( [book1, book2] ) customer.expects ...
-
Run the tests. They fail as we do not yet have a Logger class:
$ npm test > TinyMock@ test /Users/vagrant/tutorial > cake test /Users/vagrant/tutorial/Cakefile:13 throw err; ^ Error: Command failed: module.js:340 throw err; ^ Error: Cannot find module '../src/Logger' at Function.Module._resolveFilename (module.js:338:15) ...
-
Create the source code file src/Logger.coffee and implement the constructor that our first test is for:
class Logger constructor: (options) -> @log_filename = options.get_log_filename() exports.Logger = Logger
-
Run the tests again. This time they pass:
$ npm test > TinyMock@ test /Users/vagrant/tutorial > cake test Logger constructor(options) ✓ retrieves the log file name from 'options' 1 passing (13ms)
-
Write a test for a Logger.log() method ():
chai = require("chai") should = chai.should() mock = require("TinyMock") Logger = require("../src/Logger")
This time, instead of using one of the mock objects created and passed in by mock(), we use an existing object, fs, and specify the expectation on it. The expectation replaces the original appendFileSync() fs function; it does not, for example, create a spy. The original appendFileSync() function is restored just before the mock() method returns.describe "Logger", -> describe "constructor(options)", -> it "retrieves the log file name from 'options'", -> mock (options) -> options.expects("get_log_filename") logger = new Logger(options)fs = require("fs")
describe "log(message)", -> it "writes 'message' to 'log_filename' that was passed to the constructor", -> options = { get_log_filename: -> "log.txt" } logger = new Logger(options) mock -> fs.expects("appendFileSync").args("log.txt", "a message") logger.log("a message")
-
Run the tests:
$ node test > TinyMock@ test /Users/vagrant/tutorial > cake test /Users/vagrant/tutorial/Cakefile:13 throw err; ^ Error: Command failed: 1 failing 1) Logger log(message) writes 'message' to 'log_filename' that was passed to the constructor: TypeError: Object #
has no method 'log' at /Users/vagrant/tutorial/test/Logger.test.coffee:35:25 ... -
Add the log() method to the Logger class:
class Logger constructor: (options) -> @log_filename = options.get_log_filename()
fs = require("fs")
exports.Logger = Loggerlog: (message) -> fs.appendFileSync(@log_filename, message)
-
Run the tests:
$ node test > TinyMock@ test /Users/vagrant/tutorial > cake test Logger constructor(options) ✓ retrieves the log file name from 'options' log(message) ✓ writes 'message' to 'log_filename' that was passed to the constructor 2 passing (10ms)
-
log() should not eat exceptions thrown by fs.appendFileSync(), so write another test ():
chai = require("chai") should = chai.should() mock = require("TinyMock") Logger = require("../src/Logger") fs = require("fs") describe "Logger", -> describe "constructor(options)", -> it "retrieves the log file name from 'options'", -> mock (options) -> options.expects("get_log_filename") logger = new Logger(options) describe "log(message)", -> it "writes 'message' to 'log_filename' that was passed to the constructor", -> options = { get_log_filename: -> "log.txt" } logger = new Logger(options) mock -> fs.expects("appendFileSync").args("log.txt", "a message") logger.log("a message")
it "does not eat exceptions thrown by fs.appendFileSync", -> options = { get_log_filename: -> "log.txt" } logger = new Logger(options) mock -> fs.expects("appendFileSync").args("log.txt", "a message").throws(new Error("an error")) (-> logger.log("a message") ).should.throw("an error")
-
Run the tests:
$ node test > TinyMock@ test /Users/vagrant/tutorial > cake test Logger constructor(options) ✓ retrieves the log file name from 'options' log(message) ✓ writes 'message' to 'log_filename' that was passed to the constructor ✓ does not eat exceptions thrown by fs.appendFileSync 3 passing (14ms)
They pass, so log() is already passing exceptions back up to its caller. -
Refactor the tests to remove some duplication:
chai = require("chai") should = chai.should() mock = require("TinyMock") Logger = require("../src/Logger") fs = require("fs") describe "Logger", -> describe "constructor(options)", -> it "retrieves the log file name from 'options'", -> mock (options) -> options.expects("get_log_filename") logger = new Logger(options) describe "write(message)", ->
it "writes 'message' to 'log_filename' that was passed to the constructor", -> mock -> fs.expects("appendFileSync").args("log.txt", "a message") logger.log("a message") it "does not eat exceptions thrown by fs.appendFileSync", -> mock -> fs.expects("appendFileSync").args("log.txt", "a message").throws(new Error("an error")) (-> logger.log("a message") ).should.throw("an error")logger = undefined beforeEach -> options = { get_log_filename: -> "log.txt" } logger = new Logger(options)
-
Run the tests one last time:
$ node test > TinyMock@ test /Users/vagrant/tutorial > cake test Logger constructor(options) ✓ retrieves the log file name from 'options' write(message) ✓ writes 'message' to 'log_filename' that was passed to the constructor ✓ does not eat exceptions thrown by fs.appendFileSync 3 passing (17ms)