Tutorial

  1. 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.

  2. Create a project directory, and create subdirectories for source code and test code files:
    $ mkdir -p ~/tutorial/src ~/tutorial/test
    $ cd ~/tutorial
  3. 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
  4. 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"
      }
    }
  5. 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.
  6. 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")
  7. 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")
    
    
    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() 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:
    mock (store, inventory, book1, book2, customer) ->
      store.expects("get_inventory").returns(inventory)
      inventory.expects("get_books").returns( [book1, book2] )
      customer.expects ...
  8. 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)
          ...
  9. 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
  10. 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)
                  
  11. Write a test for a Logger.log() method ():
    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")
    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.
  12. 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
          ...
    
  13. Add the log() method to the Logger class:
    fs = require("fs")
    class Logger constructor: (options) -> @log_filename = options.get_log_filename()
    log: (message) ->
        fs.appendFileSync(@log_filename, message)
    exports.Logger = Logger
  14. 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)
                  
  15. 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")
  16. 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.
  17. 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)", ->
    
    logger = undefined
    
        beforeEach ->
          options = { get_log_filename: -> "log.txt" }
          logger = new Logger(options)
    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")
  18. 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)