Sinon.JS

Standalone test spies, stubs and mocks for JavaScript.
No dependencies, works with any unit testing framework.

2014/12/12 - Changelog - More builds/versions

Getting started

The following function takes a function as its argument and returns a new function. You can call the resulting function as many times as you want, but the original function will only be called once:

function once(fn) {
    var returnValue, called = false;
    return function () {
        if (!called) {
            called = true;
            returnValue = fn.apply(this, arguments);
        }
        return returnValue;
    };
}

Spies

Testing this function can be quite elegantly achieved with a test spy:

it("calls the original function", function () {
    var callback = sinon.spy();
    var proxy = once(callback);

    proxy();

    assert(callback.called);
});

The fact that the function was only called once is important:

it("calls the original function only once", function () {
    var callback = sinon.spy();
    var proxy = once(callback);

    proxy();
    proxy();

    assert(callback.calledOnce);
    // ...or:
    // assert.equals(callback.callCount, 1);
});

We also care about the this object and arguments:

it("calls original function with right this and args", function () {
    var callback = sinon.spy();
    var proxy = once(callback);
    var obj = {};

    proxy.call(obj, 1, 2, 3);

    assert(callback.calledOn(obj));
    assert(callback.calledWith(1, 2, 3));
});

Learn more about spies.

Stubs

The function returned by once should return whatever the original function returns. To test this, we create a stub:

it("returns the return value from the original function", function () {
    var callback = sinon.stub().returns(42);
    var proxy = once(callback);

    assert.equals(proxy(), 42);
});

Conveniently, stubs can also be used as spies, e.g. we can query them for their callCount, received args and more.

Learn more about stubs.

Testing Ajax

The following function triggers network activity:

function getTodos(listId, callback) {
    jQuery.ajax({
        url: "/todo/" + listId + "/items",
        success: function (data) {
            // Node-style CPS: callback(err, data)
            callback(null, data);
        }
    });
}

To test this function without triggering network activity we could stub jQuery.ajax:

after(function () {
    // When the test either fails or passes, restore the original
    // jQuery ajax function (Sinon.JS also provides tools to help
    // test frameworks automate clean-up like this)
    jQuery.ajax.restore();
});

it("makes a GET request for todo items", function () {
    sinon.stub(jQuery, "ajax");
    getTodos(42, sinon.spy());

    assert(jQuery.ajax.calledWithMatch({ url: "/todo/42/items" }));
});

Fake XMLHttpRequest

While the preceding test shows off some nifty Sinon.JS tricks, it is too tightly coupled to the implementation. When testing Ajax, it is better to use Sinon.JS' fake XMLHttpRequest:

var xhr, requests;

before(function () {
    xhr = sinon.useFakeXMLHttpRequest();
    requests = [];
    xhr.onCreate = function (req) { requests.push(req); };
});

after(function () {
    // Like before we must clean up when tampering with globals.
    xhr.restore();
});

it("makes a GET request for todo items", function () {
    getTodos(42, sinon.spy());

    assert.equals(requests.length, 1);
    assert.match(requests[0].url, "/todo/42/items");
});

Learn more about fake XMLHttpRequest.

Fake server

The preceding example shows how flexible this API is. If it looks too laborous, you may like the fake server:

var server;

before(function () { server = sinon.fakeServer.create(); });
after(function () { server.restore(); });

it("calls callback with deserialized data", function () {
    var callback = sinon.spy();
    getTodos(42, callback);

    // This is part of the FakeXMLHttpRequest API
    server.requests[0].respond(
        200,
        { "Content-Type": "application/json" },
        JSON.stringify([{ id: 1, text: "Provide examples", done: true }])
    );

    assert(callback.calledOnce);
});

Test framework integration can typically reduce boilerplate further. Learn more about the fake server.

Faking time

"I don't always bend time and space in unit tests, but when I do, I use Buster.JS + Sinon.JS"

Brian Cavalier, Cujo.JS

Testing time-sensitive logic without the wait is a breeze with Sinon.JS. The following function throttles another function - only when it has not been called for 100 milliseconds will it call the original function with the last set of arguments it received.

function throttle(callback) {
    var timer;
    return function () {
        clearTimeout(timer);
        var args = [].slice.call(arguments);
        timer = setTimeout(function () {
            callback.apply(this, args);
        }, 100);
    };
}

Thanks to Sinon.JS' time-bending abilities, testing it is easy:

var clock;

before(function () { clock = sinon.useFakeTimers(); });
after(function () { clock.restore(); });

it("calls callback after 100ms", function () {
    var callback = sinon.spy();
    var throttled = throttle(callback);

    throttled();

    clock.tick(99);
    assert(callback.notCalled);

    clock.tick(1);
    assert(callback.calledOnce);

    // Also:
    // assert.equals(new Date().getTime(), 100);
}

As before, Sinon.JS provides utilities that help test frameworks reduce the boiler-plate. Learn more about fake time.

And all the rest...

You've seen the most common tasks people tackle with Sinon.JS, yet we've only scratched the surface. View more quick examples below, or dive into the API docs, which also provides useful pointers on how and when to use the various functionality.

Features at a glance

// Function under test
function once(fn) {
    var returnValue, called = false;
    return function () {
        if (!called) {
            called = true;
            returnValue = fn.apply(this, arguments);
        }
        return returnValue;
    };
}

it("calls the original function", function () {
    var spy = sinon.spy();
    var proxy = once(spy);

    proxy();

    assert(spy.called);
});

Spy documentation.

// Function under test
function once(fn) {
    var returnValue, called = false;
    return function () {
        if (!called) {
            called = true;
            returnValue = fn.apply(this, arguments);
        }
        return returnValue;
    };
}

it("returns the return value from the original function", function () {
    var stub = sinon.stub().returns(42);
    var proxy = once(stub);

    assert.equals(proxy(), 42);
});

Stub documentation.

// Function under test
function once(fn) {
    var returnValue, called = false;
    return function () {
        if (!called) {
            called = true;
            returnValue = fn.apply(this, arguments);
        }
        return returnValue;
    };
}

it("returns the return value from the original function", function () {
    var myAPI = { method: function () {} };
    var mock = sinon.mock(myAPI);
    mock.expects("method").once().returns(42);

    var proxy = once(myAPI.method);

    assert.equals(proxy(), 42);
    mock.verify();
});

Mock documentation.

// Function under test
function throttle(callback) {
    var timer;
    return function () {
        clearTimeout(timer);
        var args = [].slice.call(arguments);
        timer = setTimeout(function () {
            callback.apply(this, args);
        }, 100);
    };
}

var clock;

before(function () { clock = sinon.useFakeTimers(); });
after(function () { clock.restore(); });

it("calls callback after 100ms": function () {
    var callback = sinon.spy();
    var throttled = throttle(callback);

    throttled();

    clock.tick(99);
    assert(callback.notCalled);

    clock.tick(1);
    assert(callback.calledOnce);

    // Also:
    // assert.equals(new Date().getTime(), 100);
}

Timers documentation

// Function under test
function getTodos(listId, callback) {
    jQuery.ajax({
        url: "/todo/" + listId + "/items",
        success: function (data) {
            // Node-style CPS: callback(err, data)
            callback(null, data);
        }
    });
}

var xhr, requests;

before(function () {
    xhr = sinon.useFakeXMLHttpRequest();
    requests = [];
    xhr.onCreate = function (req) { requests.push(req); };
});

after(function () {
    // Like before we must clean up when tampering with globals.
    xhr.restore();
});

it("makes a GET request for todo items", function () {
    getTodos(42, sinon.spy());

    assert.equals(requests.length, 1);
    assert.match(requests[0].url, "/todo/42/items");
});

Fake XHR/server documentation

// Function under test
function getTodos(listId, callback) {
    jQuery.ajax({
        url: "/todo/" + listId + "/items",
        success: function (data) {
            // Node-style CPS: callback(err, data)
            callback(null, data);
        }
    });
}

var server;

before(function () { server = sinon.fakeServer.create(); });
after(function () { server.restore(); });

it("calls callback with deserialized data", function () {
    var callback = sinon.spy();
    getTodos(42, callback);

    // This is part of the FakeXMLHttpRequest API
    server.requests[0].respond(
        200,
        { "Content-Type": "application/json" },
        JSON.stringify([{ id: 1, text: "Provide examples", done: true }])
    );

    assert(callback.calledOnce);
});

Fake XHR/server documentation

// Function under test
function once(fn) {
    var returnValue, called = false;
    return function () {
        if (!called) {
            called = true;
            returnValue = fn.apply(this, arguments);
        }
        return returnValue;
    };
}

it("returns the return value from the original function", sinon.test(function () {
    var myAPI = { method: function () {} };
    var mock = sinon.mock(myAPI);
    mock.expects("method").once().returns(42);

    var proxy = once(myAPI.method);

    assert.equals(proxy(), 42);
}));

sinon.test() provides automatic restoring of stubbed/mocked globals, automatic mock verification and more. Sandbox documentation

it("makes a GET request for todo items", function () {
    sinon.stub(jQuery, "ajax");
    getTodos(42, sinon.spy());

    sinon.assert.calledOnce(jQuery.ajax);
    sinon.assert.calledWithMatch(jQuery.ajax, { url: "/todo/42/items" });
});

Assertions documentation

it("tests partial argument matches, regexen and more", function () {
    var spy = sinon.spy();
    spy("Go", function () { /* ... */ });

    assert(spy.calledWith("message", sinon.match.func));
    assert(spy.calledWithMatch(/[a-z]+/));
});

Matcher documentation.

Documentation

Test framework adapters

Get help

Sinon.JS elsewhere

My book, Test-Driven JavaScript Development covers some of the design philosophy and initial sketches for Sinon.JS.

Sinon uses Semantic versioning.

Copyright 2010 - 2014, Christian Johansen. Released under the BSD license.