位元詩人 技術雜談:Test-Driven Development (TDD) in CoffeeScript with Jasmine

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

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
{{< / highlight >}}

Let's say that we want to practice how to implement [hash table](http://en.wikipedia.org/wiki/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.

```console
$ mkdir hash_ex
$ cd hash_ex
$ vim package.json   # or other your favorite editor
{{< / highlight >}}

The content of *package.json* is like this:

```javascript
{
    "dependencies": {
    },
    "devDependencies": {
        "grunt": "*",
        "grunt-jasmine-node": "*"
    }
}
{{< / highlight >}}

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:

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

  grunt.loadNpmTasks 'grunt-jasmine-node'

  grunt.registerTask 'default', ['jasmine_node']
{{< / highlight >}}

Now, install local Node.js packages with `npm`:

```console
$ npm install
{{< / highlight >}}

Then, initialize Jasmine tests:

```console
$ jasmine init
{{< / highlight >}}

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

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

Done, without errors.
{{< / highlight >}}

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.

```console
$ mkdir lib
$ mkdir spec/lib
$ vim spec/lib/hash_spec.coffee  # or other your favorite editor
{{< / highlight >}}

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

```coffeescript
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

{{< / highlight >}}

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.

```coffeescript
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
{{< / highlight >}}

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.

```console
$ 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.
{{< / highlight >}}

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

```coffeescript
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

{{< / highlight >}}

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:

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

Done, without errors.
{{< / highlight >}}

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.

```coffeescript
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"
{{< / highlight >}}

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

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

Done, without errors.
{{< / highlight >}}

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.
關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。