Fork me on GitHub

JSpec JavaScript Testing Framework

JSpec is a extremely small, yet very powerful testing framework. Utilizing its own custom grammar and pre-processor, JSpec can operate in ways that no other JavaScript testing framework can. This includes many helpful shorthand literals, a very intuitive / readable syntax, as well as not polluting core object prototypes.

JSpec can also be run suites in a variety of ways, such as via the terminal with Rhino support, via browsers using the DOM or Console formatters, or finally by using the Ruby JavaScript server which runs browsers in the background, reporting back to the terminal.

Features

Cheat Sheet

Using the bash cheat sheet library you can get quick terminal access to JSpec's cheat sheet and hundreds more.

cd /tmp && git clone git://github.com/visionmedia/ch.git && cd ch && sudo make install
ch jspec

Screencasts

Example

describe 'ShoppingCart' before_each cart = new ShoppingCart end describe 'addProduct' it 'should add a product' cart.addProduct('cookie') cart.addProduct('icecream') cart.should.have 2, 'products' end end describe 'addProducts' it 'should add several products' cart.should.receive('addProduct', 'twice').with_args(an_instance_of(String)) cart.addProducts('cookie', 'icecream') end end describe 'checkout' it 'throw an error when checking out with no products' -{ cart.clear().checkout() }.should.throw_error CheckoutError, 'No products' end end end

Example Without Grammar

JSpec.describe('ShoppingCart', function(){ before_each(function(){ cart = new ShoppingCart }) describe('addProduct', function(){ it('should add a product', function(){ cart.addProduct('cookie') cart.addProduct('icecream') cart.should.have 2, 'products' }) }) describe('addProducts', function(){ it('should add several products', function(){ cart.should.receive('addProduct', 'twice').with_args(an_instance_of(String)) cart.addProducts('cookie', 'icecream') }) }) describe('checkout', function(){ it('throw an error when checking out with no products', function(){ -{ cart.clear().checkout() }.should.throw_error CheckoutError, 'No products' }) }) })

DOM Formatter

The DOM formatter is the default of JSpec, featuring assertion graphs and a sleek white style.

JSpec DOM Formatter

Matchers

Click a matcher below to view an example

Core

  • be
  • eql
  • equal
  • be_a
  • be_an
  • be_an_instance_of
  • be_at_least
  • be_at_most
  • be_within
  • be_null
  • be_undefined
  • be_empty
  • be_true
  • be_false
  • be_type
  • be_greater_than
  • be_less_than
  • have
  • have_at_least
  • have_at_most
  • have_within
  • have_length
  • have_prop
  • have_property
  • include
  • match
  • throw_error
  • respond_to

jQuery

  • have_tag
  • have_one
  • have_tags
  • have_many
  • have_child
  • have_children
  • have_text
  • have_attr
  • have_value
  • have_class
  • have_classes
  • be_visible
  • be_hidden
  • be_enabled
  • be_disabled
  • be_selected
  • be_checked
  • have_type
  • have_id
  • have_title
  • have_alt
  • have_href
  • have_src
  • have_rel
  • have_rev
  • have_name
  • have_target

JSpec Executable

The packaged JSpec executable allows you to quickly initialize project templates which utilize JSpec, update them with the lateast versions of JSpec, as well as auto-run suites in multiple browsers when a file is altered. For information beyond the examples below consult `jspec help`.

# Initialize template in the current directory 
$ jspec init

# Initialize template at ./myproject
$ jspec init myproject 

# Initialize with a symlink of the current version of JSpec at spec/lib
$ jspec init --symlink

# Initialize with a localized copy of JSpec at spec/lib
$ jspec init --freeze
# Update to the lateast version of JSpec within your spec suite
# this works with --freeze and --symlink, as well as just a regular
# project that points to the JSpec gemdir.
$ jspec update

# Update a specific file
$ jspec update spec/run-suites.html
# Refresh suites when any JavaScript is altered
$ jspec

# View help
$ jspec help

# View command specific help
$ jspec help run

# Run suites once in the default browser (Safari)
$ jspec run

# Run suites once in the browsers passed
$ jspec run --browsers Firefox,Explorer,Opera

# Alternate naming
$ jspec run --browsers ff,ie,opera

# Refresh suites when altered in a custom suite path, in several browsers
$ jspec spec/run-suites.html --browsers Safari,Firefox

# Run custom suite path once
$jspec run spec/some-suite.html

# Run Ruby server in several browsers (reports back to terminal)
$ jspec run --server --browsers Safari,Opera,Firefox

$ Lowercase works too!
$ jspec run --server --browsers safari,opera,ff,chrome

# Run Ruby server in all supported browsers
$ jspec run --server

# Run suites using Rhino
$ jspec run --rhino

# Install Rhino .jar
$ jspec install rhino

# Install jQuery 1.3.1
$ jspec install jquery --release 1.3.1

Options

The follow options may be passed to JSpec.run()

Growl Support

JSpec uses the JavaScript Growl library to provide growl support when using Rhino for testing. Simply load() jspec.growl.js within spec/rhino.js

Proxy Assertions or 'Spies'

JSpec's support for proxy assertions allows you to assert that a method is invoked a specific number of times, with specific arguments, returning specific results. In the example below, we assert that getDogs() calls getPets() with a string of 'dogs', and returns an array of dogs. All these assertions must pass in order for the spec to pass.

person.should.receive('getPets', 'once').with_args('dogs').and_return(an_instance_of(Array))
dogs = person.getDogs()

The example below will fail, due to the method being called only once with a string.

person.should.receive('getPets', 'twice').with_args(an_instance_of(String))
dogs = person.getPets('dogs') // Passes
all  = person.getPets() // Fails

Fixtures

JSpec's fixture support is simple, yet elegant and powerful. The fixture() utility works for both Rhino and browsers alike. Paths are resolved as follows:

fixture('FILE')

This means that if you have an html fixture located at 'spec/fixtures/foo.html' you may simply call fixture('foo'), likewise when using other extensions you may simply use fixture('foo.json'). When fixtures reside in different directories this may be specified as well fixture('data/foo.html'), fixture('html/foo.html'), etc.

Method Stubbing

JSpec provides the stub() function in order to alter a method for the duration of a specification (it block).

stub(person, 'toString').and_return('Ive been stubbed!')

After a spec has run, JSpec uses destub() to remove a specific stubbed method.

destub(person, 'toString')

At any time we could destub all stubbed methods for an object

destub(person)

Alternatively those using the JSpec grammar may use the psuedo methods

person.stub('age').and_return(22)

Which is then converted to the regular function calls we saw above.

DOM Testing With jQuery

When using jQuery it is extremely simple to test how your library interacts with a mock DOM. As you can see below there is no need to add arbitrary elements to your suite HTML, jQuery's constructor will generate the elements from a string or fixture.

describe 'DOM'
  before_each
    // Load spec/fixtures/user-ul-list.html fixture
    list = $(fixture('user-ul-list'))
  end
  
  describe 'jQuery.hide()'
    it 'hide elements'
      list.hide().should.be_hidden
    end
  end
end

Click to see an example spec suite from the jQuery.inline-search.js plugin

Mock Ajax Requests

jspec.xhr.js provides the mock_request() and unmock_request() utilities. unmock_request() is automatically called after each spec, so it is usually not called manually. unmock_request() simply restores the original XMLHttpRequest object. mock_request() is framework agnostic, however below it is shown in use with jQuery:

describe 'Something'
  before_each
    // type, status, and headers are optional
    mock_request().and_return('{ foo: "bar" }', 'application/json', 200, { Accept: 'foo' })
  end
  
  it 'should foo'
    $.getJSON('foo', function(response){
      response.foo.should.eql 'bar'
    })
  end
end

Async Support With Mock Timers

The JavaScript mock timer library http://github.com/visionmedia/js-mock-timers is bundled with JSpec and is located at 'lib/jspec.timers.js'.

Timers return ids as expected, which may be terminated when passed to clearInterval() however mock timers

require manual scheduling and progression via the tick() function.
setTimeout(function(){
  alert('Wahoo!')
}, 400)

tick(200) // Nothing happens
tick(400) // Wahoo!

When using setInterval(), providing a large millisecond increment to tick() all callbacks which have not had a change to 'catch up' may be called several times as conveyed in the example below.

progress = ''
var id = setInterval(function(){
  progress += '.'
}, 100)

tick(50),  print(progress) // ''
tick(50),  print(progress) // '.'
tick(100), print(progress) // '..'
tick(100), print(progress) // '...'
tick(300), print(progress) // '......'

clearInterval(id)

tick(800) // Nothing happens

Finally a practical example:

function poll(uri, fn) {
  setInterval(fn || function() {
    // ... xhr request
  }, 2000)
}

describe 'poll()'
  it 'should poll a uri every 2 seconds'
    called = 0
    poll('http://vision-media.ca', function(){
      ++called
    })
    tick(8000)
    called.should.eql 4
  end
end

Extending JSpec With Modules

JSpec's Modules take the form of a simple JavaScript object or 'hash', allowing you to add matchers

define dependencies, add utility functions, implement hook callbacks and more. Below is an example module taken from core to add jQuery support. Consult the README for additional information.
// JSpec - jQuery - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)

JSpec
.requires('jQuery', 'when using jspec.jquery.js')
.include({
  
  // --- Initialize
  
  init : function() {
    jQuery.ajaxSetup({ async : false })
  },
  
  // --- Utilities
  
  utilities : {
    element : jQuery,
    elements : jQuery,
    sandbox : function() {
      return jQuery('<div class="sandbox"></div>')
    }
  },
  
  // --- Matchers
  
  matchers : {
    have_tag      : "jQuery(expected, actual).length == 1",
    have_one      : "alias have_tag",
    have_tags     : "jQuery(expected, actual).length > 1",
    have_many     : "alias have_tags",
    have_child    : "jQuery(actual).children(expected).length == 1",
    have_children : "jQuery(actual).children(expected).length > 1",
    have_text     : "jQuery(actual).text() == expected",
    have_value    : "jQuery(actual).val() == expected",
    be_enabled    : "!jQuery(actual).attr('disabled')",
    have_class    : "jQuery(actual).hasClass(expected)",
    
    be_visible : function(actual) {
      return jQuery(actual).css('display') != 'none' &&
             jQuery(actual).css('visibility') != 'hidden' &&
             jQuery(actual).attr('type') != 'hidden'
    },
    
    be_hidden : function(actual) {
      return !JSpec.does(actual, 'be_visible')
    },

    have_classes : function(actual) {
      return !JSpec.any(JSpec.argumentsToArray(arguments, 1), function(arg){
        return !JSpec.does(actual, 'have_class', arg)
      })
    },

    have_attr : function(actual, attr, value) {
      return value ? jQuery(actual).attr(attr) == value:
                     jQuery(actual).attr(attr)
    },
    
    'be disabled selected checked' : function(attr) {
      return 'jQuery(actual).attr("' + attr + '")'
    },
    
    'have type id title alt href src sel rev name target' : function(attr) {
      return function(actual, value) {
        return JSpec.does(actual, 'have_attr', attr, value)
      }
    }
  }
})

Module Hooks

Hooks are called throughout JSpec's execution to provide modles with a chance to alter, or act upon

what is currently being processed. For example to add range support to JSpec we can simply implement a hook callback for the 'preprocessing' hook:

This implementation would allow '0..5' to be expanded to '[0,1,2,3,4,5]'

JSpec.include({
  preprocessing : function(input) {
    return input.replace(/(\d+)\.\.(\d+)/g, function(_, start, end){
      var current = parseInt(start), end = parseInt(end), values = [current]
      if (end > current) while (++current <= end) values.push(current)
      else               while (--current >= end) values.push(current)
      return '[' + values + ']'
    })
  }
})

Grammar Rules

Below are the conversions made when utilizing the JSpec grammar, literal JavaScript is un-touched.

Assertions

An assertion without parens

'foobar'.should.include 'bar'

Is converted to include parens

'foobar'.should.include('bar')

Which is then converted to a literal grammar-less JSpec assertion.

expect('foobar').to(include, 'bar')

Function Literal

The following are equivalent

-{ throw 'test' }.should.throw_error
function(){ throw 'test' }.should.throw_error

Range Literal

Below is the inclusive range literal

1..5

Which expands to

[1,2,3,4,5]

May be used to create descriptive matchers

n.should.be_within 5..9

__END__ Token

Any text placed after the __END__ token is considered irrelevant and is striped out before evaluation. This is sometimes useful for document or code reference while writing specs.

For example when writting regression specs it is sometimes useful to paste the issue ticket's comment(s) below this area for reference.

describe 'some issue'
  it 'should work'
    true.should.be_true
  end
end

__END__
anything here, text,
code, you name it! :)

Extending the jspec executable

Both project specific and user specific sub-commands may be used to extend those already provided by jspec. For example add the following to spec/commands/example_commands.rb which is then loaded when jspec is executed. Or place it in ~/jspec/commands

command :example do |c|
  c.syntax = 'jspec example [options]'
  c.description = 'Just an example command'
  c.option '-f', '--foo string', 'Does some foo with '
  c.option '-b', '--bar [string]', 'Does some bar with [string]'
  c.example 'Do some foo', 'jspec example --foo bar'
  c.example 'Do some bar', 'jspec example --bar'
  c.when_called do |args, options|
    p args
    p options.__hash__
    # options.foo
    # options.bar
    # options.__hash__[:foo]
    # options.__hash__[:bar]
  end 
end

More Information

Authors

TJ Holowaychuk (tj@vision-media.ca)