技術雜談: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

    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.

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk