coffeescript  

Feb 9, 2015 • Michael Chen

Test-driven developememnt (TDD) is a software development process. In this process, you write automated tests for expected functions; then, you write minimal code that satisfy these tests; finally, you refactor your code to meet your need (and still suffice your tests. Initially, developing in TDD way needs to write some extra code as tests; however, bugs can be reduced by fulfilling these tests during coding process. To simplifiy your testing tasks, using a testing framework is recommended. Here we introduce TDD in CoffeeScript with Jasmine.

Jasmine is a testing framework for JavaScript. I prefer Jasmine to other JavaScript testing frameworks because it share similiar syntax with RSpec, a testing framework for Ruby; therefore, it reduces re-learning time. However, Jasmine doesn’t support CoffeeScript per se. There are several choices to solve the problem. You can compile your CoffeeScript files into corresponding JavaScript ones and test these files. Nevertheless, repetitive command typing is not productive. Accordingly, I’ll introduce Grunt to automate our TDD process.

Before we start our demo, install Jasmine, Grunt and CoffeeScript. (CoffeeScript itself is not strictly needed in our demo. But it’s fine to install it, since we want to do CoffeeScript programming.)

$ npm install jasmine -g
$ npm install grunt-cli -g
$ npm install coffee-script -g

Let’s say that we want to practice how to implement hash table in CoffeeScript. (Don’t do this in practice. JavaScript has built-in support for hash table in JavaScript object.) Create a new directory; go into it; create a package.json file in the root of this folder. We want to create a standard Node.js package in our demo.

$ mkdir hash_ex
$ cd hash_ex
$ vim package.json   # or other your favorite editor

The content of package.json is like this:

{
    "dependencies": {
    },
    "devDependencies": {
        "grunt": "*",
        "grunt-jasmine-node": "*"
    }
}

To use Grunt, we need a Gruntfile as a automation script. Think it a list for possible automation tasks in this project. A nice feature of Grunt is its support for CoffeeScript; therefore, we can exploit this feature and write a Gruntfile.coffee file here:

{
module.exports = (grunt) ->
  grunt.initConfig(
    jasmine_node:
      options:
        coffee: true
        extensions: 'coffee'
      all: ['spec/']
  )

  grunt.loadNpmTasks 'grunt-jasmine-node'

  grunt.registerTask 'default', ['jasmine_node']

Now, install local Node.js packages with npm:

$ npm install

Then, initialize Jasmine tests:

$ jasmine init

If the project is properly set, we can start some tests:

$ grunt
Finished in 0.001 seconds
0 tests, 0 assertions, 0 failures

Done, without errors.

Since there was no any test yet, there was no error occurred here. Before we start to write our hash code, let’s write some tests.

$ mkdir lib
$ mkdir spec/lib
$ vim spec/lib/hash_spec.coffee  # or other your favorite editor

We want to test whether our hash is functional or not, so write down some tests in spec/lib/hash_spec.coffee.

Hash = require '../../lib/hash.coffee'

describe "hash", ->
  it "create an empty hash", ->
    h = new Hash
    expect(h).toEqual jasmine.any(Hash)

  it "no such value in a empty hash", ->
    h = new Hash
    expect(h.get "one").toBe undefined

  it "get value from a hash", ->
    h = new Hash
    h.put "one", "eins"
    h.put "two", "zwei"
    h.put "three", "drei"
    expect(h.get "one").toMatch "eins"

  it "no such value in a hash", ->
    h = new Hash
    h.put "one", "eins"
    h.put "two", "zwei"
    h.put "three", "drei"
    expect(h.get "four").toBe undefined

There is no pre-defined way to write tests. Generally, you should write tests for both positive and negative conditions. Let’s write some real code. Add lib/hash.coffee in our project; write the constructor of our Hash class. We’ll use separated chaining in our hash.

class Hash
  prime = 97

  ###*
  # Hash table implemented in separated chaining
  ###
  constructor: ->
    @hash = ( ->
      array = new Array(prime)
      for i in [0..(array.length - 1)]
        array[i] = []
      return array
    )()

module.exports = Hash

Here we wrote a function object and immediately got its result. What we wanted was the array, not the function itself, so these parentheses were needed. Then, test our code again. You should see some failed tests here. Don’t worry. TDD way is meant to fulfill these failed tests.

$ grunt
Finished in 0.008 seconds
4 tests, 4 assertions, 3 failures

Warning: Task "jasmine_node:all" failed. Use --force to continue.

Aborted due to warnings.

Let’s implement our hash table. Edit lib/hash.coffee again:

class Hash
  # our constructor is here

  hash_function = (key) ->
    num = 0
    for e in key
      num += num * 7 + e.charCodeAt()
    return num

  get: (key) ->
    num = hash_function key
    k = num % prime
    for i in [0..(@hash[k].length - 1)] by 2
      if key == @hash[k][i]
        return @hash[k][i+1]
    return

  put: (key, value) ->
    num = hash_function key
    k = num % prime
    for i in [0..(@hash[k].length - 1)] by 2
      if key == @hash[k][i]
        @hash[k][i+1] = value
        return
    @hash[k].push key
    @hash[k].push value
    return

We wrote an inner function here. Our tests wouldn’t touch this function. The spirit of TDD is separating interfaces from implementation. We used separating chaining to implement our hash table, but we didn’t do rehashing in this demo.

Test our code again:

Finished in 0.006 seconds
4 tests, 4 assertions, 0 failures

Done, without errors.

Great. No further error occurred. But we had another question here: if our value was replaced by another one, the hash still worked? When in doubt, write tests.

Hash = require '../../lib/hash.coffee'

describe "hash", ->
  # old tests are here
    
  it "replace value in a hash", ->
    h = new Hash
    h.put "one", "eins"
    h.put "two", "zwei"
    h.put "three", "drei"
    h.put "one", "une"
    expect(h.get "one").toMatch "une"

Let’s do more tests. The result revealed that our code worked well.

$ grunt
Finished in 0.006 seconds
5 tests, 5 assertions, 0 failures

Done, without errors.

With the combination of Jasmine and Grunt, you can write CoffeeScript code in TDD way. Let TDD be your friends; you’ll get better code.