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
# Update to the lateast version of JSpec within your spec suite
$ 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

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.

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 + ']'
    })
  }
})

More Information

Authors

TJ Holowaychuk (tj@vision-media.ca)