Sinon.js: Spies, Stubs, Mocks and Fakes
Sinon.js is a library to help with testing in Javascript. I was looking at the examples and most of the examples looked very similar to me. The differences seemed subtle. So here is my explanation for myself on what each is and when to use what. I have copied most of the examples from their docs just to make the typing easier for me.
If you are in a hurry or want to skip the explanations, scroll down to the conclusion section and see what each means and when should they be used.
Spies are exactly that. They are useful to spy in on objects to check what was called etc. An example of where that can be useful is to check if a callback was called.
"test should call subscribers on publish": function () { var callback = sinon.spy(); PubSub.subscribe("message", callback); PubSub.publishSync("message"); assertTrue(callback.called); }
As you can see above, callback is a spy object. We can check it was called by checking the "called" property on it.
Just like any good spy, the spy can do multiple things. There is a whole tonne of things, I'll ask you to refer to the docs page for that.
What you need to know is when you want to inspect the state of an object, call etc., you would use a spy.
Stubs provide an alternate functionality in existing objects. One thing to note is a Stub also implements the whole Spy API. So everything a spy can do, the stub can as well.
Here is an example of its use:
"test should stub method differently based on arguments": function () { var callback = sinon.stub(); callback.withArgs(42).returns(1); callback.withArgs(1).throws("TypeError"); callback(); // No return value, no exception callback(42); // Returns 1 callback(1); // Throws TypeError }
Above, callback is a stub. We give it some behaviors - when it is called with 42 as the argument, it returns 1; if it is called with 1, it throws an error. otherwise, returns nothing.
You would generally want to stub functionality that has many dependencies or is expensive/unreliable (e.g. reading over the network). Stubs are used to provide alternate functionality to existing objects.
Mocks are the same as stubs, except they have expectations baked in. So when an expectation is not fulfilled, they will fail your test. Also, Mocks implement both the Spy and Stub APIs.
sinon.mock(jQuery).expects("ajax").atLeast(2).atMost(5); jQuery.ajax.verify();
The "ajax" method of the jQuery object has been mocked above. The expectation has been set that it should be called at least 2 times and at most 5 times.
You would use a mock when you want to provide both alternate functionality and an expectation in your test.
This is probably one of the easier things to understand. There may be code that relies on setTimeout/setInterval type functionality (e.g. Animation, retry logic etc.).
We wouldn't want our test to actually wait for as long as required by the timer. We can substitute a fake timer, that way speeding up time in our test (thus making the dependent code execute sooner).
{ setUp: function () { this.clock = sinon.useFakeTimers(); }, tearDown: function () { this.clock.restore(); }, "test should animate element over 500ms" : function(){ var el = jQuery("<div></div>"); el.appendTo(document.body); el.animate({ height: "200px", width: "200px" }); this.clock.tick(510); assertEquals("200px", el.css("height")); assertEquals("200px", el.css("width")); } }
The system timer is replaced with a fake timer. And you control the timing by calling the "tick" method on the clock.
When you want to test code that relies on setTimeout/setInterval, you would use the Fake timers to make the test faster and more predictable.
Matchers allow for flexible assertion. A matcher is ultimately a function that is called to verify a value. It may be passed in as an argument to spy.calledWith, spy.returned, and spy.withArgs.
"test should stub method differently based on argument types": function () { var callback = sinon.stub(); callback.withArgs(sinon.match.string).returns(true); callback.withArgs(sinon.match.number).throws("TypeError"); callback("abc"); // Returns true callback(123); // Throws TypeError }
Above, "sinon.match,string" creates a function which expects the object verified using it to be a string. So when used in the context of "callback.withArgs", it means the arguments are expected to be of type string. So when callback is called with string arguments, it returns true.
You would use a matcher, to name a common/complex assertion to make the code easier to understand/maintain.
Sandboxes simplify working with fakes. It can restore all the faked objects when restore is called on the sandbox object. You would usually never work with the Sandbox object directly. You would generally use sinon.test(fn). The call to sinon.test wraps the passed in fn and automatically calls restore at the end, reverting all the faked objects.
"test using sinon.test sandbox": sinon.test(function () { var myAPI = { method: function () {} }; this.mock(myAPI).expects("method").once(); PubSub.subscribe("message", myAPI.method); PubSub.publishSync("message", undefined); })
Note how the mock is created using "this.mock" and not "sinon.mock". The "this" in the function passed to sinon.test refers to the sandbox object above. Calling "this.mock" is important because that is how the sandbox tracks all the fakes being used, so that they can be restored at the end.
You would use a Sandbox when it is important for you to revert the faked functionality at the end of your test. Using sinon.test ensures the reversion of fakes is done automatically.
Use spies to inspect the state of an object, call etc.
Use stubs to fake functionality
Use mocks to fake functionality and specify expectations at the same time
Use fake timers when you need to test code that relies on timers (setTimeout/setInterval)
Matchers are flexible assertions. A matcher can be used to name a common/complex assertion to make the code easier to understand.
Use sinon.test (or Sandboxes) when restoration of the faked objects is important at the end of a test.