Tuesday, June 04, 2013

Unit Test like a Secret Agent with Sinon.js

The following content comes from the forth module of my Pluralsight course entitled: Front-End First: Testing and Prototyping JavaScript Apps. The rest of the course covers an introduction to Unit Testing, Examples of Hard to Test Code, Mocha (a JavaScript test runner), Grunt (a JavaScript task runner), Mockjax (a way to mock Ajax requests), mockJSON (a way to generate semi-random complex objects for prototyping), and more.

Introduction


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

Sinon.js is a really helpful library when you want to unit test your code. It supports spies, stubs, and mocks. The library has cross browser support and also can run on the server using Node.js.

Spies


“A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. A test spy can be an anonymous function or it can wrap an existing function.”

Example


A test spy records how it is used. It will record how many times it was called, what parameters were used, when it was called, and a bunch of other things. Here you can see an example of creating a spy and I've listed out only a small subset of it’s features such as called, callCount, calledWith, threw, returned, and more.


In addition to just creating a new spy, you can also take an existing function and turn it into a spy. In this example we are taking jQuery and turning it’s ajax method into a spy. Once the spy has been used you can actually pull out one of those instances and verify how that particular call was used. And again, it is important to restore the function back to it’s original state much like we did when we manually stubbed our functions previously.


Mission Impossible: Spy


In the following simple code example we are creating a new ethanHunt spy and passing it to the missionImpossible.start method.

As you can see the start method takes the agent that was passed in and immediately invokes it.

The spy will record how it is used and then you can observe what happened.


At this point we can interrogate ethanHunt if he was called or not, how many times it was called, and a bunch of other questions.


Stubs


“Test stubs are functions (spies) with pre-programmed behavior. They support the full test spy API in addition to methods which can be used to alter the stub's behavior.”

A stub in Sinon.js is also a spy as we've just seen, but it is also a function that has some predefined behavior. A stub is used when we want to fake some functionality so that our system thinks everything is performing normally.

Example


You'll see here that after we have created a stub we can optionally respond to it based on the parameters that are passed to it.


Here we are telling our stub that if "Hello" is passed to it that it should return the string "World" and if we pass "Wuz" to the stub that "Zup?" should be returned.

We can do other things like if "Kapow" is passed to our stub then an exception will be thrown and we can get even more sophisticated and say if an object is passed to the stub it should yieldTo (or invoke) the call function that was passing using the "Howdy" argument. This is some pretty serious and awesome functionality built into these stubs!

Mission Impossible: Stub


In this next mission, if you choose to accept it... we are stubbing out a tape function that will be passed into an assignment method.

The tape will either be passed the string "accept" or "reject" and depending on the answer we want a different result.

With a sinon stub, that is no problem. We can just say tape.withArgs("accept"). returns(new Mission()) and if we wanted to throw a Disintegrate exception if the tape was rejected then we just follow the same pattern... tape.withArgs("reject"). throws("Disintegrate").

If you can't tell already these stubs are really powerful and a great addition to your testing toolkit.


Once we've set up our stub, we can exercise our code as we would normally and the stub will respond with whatever behavior we predefined. Below you'll see that once we pass "accept" that we are getting a Mission object back and if we "reject" the assignment that a Disintegrate exception is thrown.


Stubbed Unit Test


Let’s take an example Twitter unit test and show how we can use a stub to simulate a response from jQuery’s ajax method.


In the before hook we will ask Sinon.js to create us a new stub based off of jQuery’s ajax method and we want to yieldTo (or invoke) the success function from the object that is passed to it. And while we are at it we want to pass our fake twitter data along with the success function.

With that one line of code we have stubbed out the jQuery ajax method and provide fake test data that we can use in our unit test.

Again, it is important to clean up after ourselves so in the after hook at the bottom here we are taking the jQuery.ajax method and calling restore which removes all of the stub behavior from the function,

Mocks


“Mocks (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations. A mock will fail your test if it is not used as expected.”

Now we finally get to mocks. Mocks are a lot like a stub and a spy, but with a slight twist. With a mock you define up front all of the things you want to expect ( or happen ) then when you are all done with your tests you assert that all those things happened as planned. So, it’s a slightly different way to think than if using a spy or stub by themselves.

Example


In the following code we are defining a mock based off our opts object and we are saying that we expect the call method should only be called once and when it is called that it should have the "Hello World" string argument passed to it.


Then we proceed to run our code that we want tested. You'll see here I’m calling the call method passing the "Hello World" string.

And then at the end you tell the mock object to mock.verify() that all of the expectations you've made previously are valid. If for some reason an expectation was not met, then an exception will occur. And then just like in most of the other examples, we need to clean up after ourselves and call the restore method off of what was mocked.

Mocked Unit Test


Let’s take another look at the Twitter getTweets unit tests again, but this time use a mock instead of a stub.


In the before hook I’m creating a mock of the jQuery object and I’m expecting that the ajax method will only be called one and that it should invoke the success method of the object I pass in with some fakeData I've provided.

Inside my unit test I run the code I want to tests, which is the getTweets method, and then on the callback I call the verify method off of the mock to make sure my expectations have been met.

And as before I restore the object in the after hook.

Fake Timers


“Fake timers is a synchronous implementation of setTimeout and friends that Sinon.JS can overwrite the global functions with to allow you to more easily test code using them.“

Another handy feature of Sinon.s is that you can fake timers! At first this might seem strange, but it turns out it is really powerful and clever.

Example


We first start by asking Sinon.js to useFakeTimers() and save off the clock it gives us. Now let’s take some jQuery animation code that will fadeIn an element slowly onto the screen.


Normally if we wanted to test if this element showed up on the screen we'd either need to provide a callback when the animation is finished or tap into the promise created from the deferred and wait for that to resolve.

However, much like a time lord we can take sinon’s TARDIS, errr... I mean fake timer and tell the clock that we are now 650 milliseconds in the future! And then we can immediately assert that the element is visible without waiting. And of course we will need to restore the clock back to normal when we are done.

Fake Server


“High-level API to manipulate FakeXMLHttpRequest instances.”

Another neat feature that Sinon.js has is a fake server. This is a high level abstraction over the FakeXMLHttpRequest that Sinon.js also provides if you need more granular support.

Example


We can create a fake server from Sinon.js, and we can define that for a GET to the /twitter/api/user.json resource we want to respond with a status code of 200 and the following JSON data.


Then if we called jQuery’s get method with the same URL then we'd get back the data we stubbed out. A key to remember is that you do need to tell the server to respond as we did immediately after we called the get method. And finally we need to restore the server when we are done.

Fake Server Unit Test


Let’s take this technique and add it to our twitter unit test.


In our before hook we create the server and match the resource that our twitter app will be calling and pass back the data we want to stub out. Then we unit test out the getTweets method as we did before, but things don't work out as we expect! Why is that? Well, it is because we are using JSONP as our jQuery ajax datatype. The way JSONP works is that it isn't actually using XMLHttpRequest as a typical Ajax call does. Instead JSONP uses some trickery of injecting a dynamic script tag on your page and a bunch of other things that jQuery tries to hide from you for simplicities sake.

So, in this case using the fake server won't help us. It would be better if we used a stub like we did in the last example.

Conclusion


Hopefully you can see that Sinon.js is a great utility library to help make unit testing a much more effective and terse experience. You'll probably more often than not find yourself making spies and stubs much more often than mocks, but that is really up to how you approach unit testing.

If you enjoyed this content you can get more from my recent Pluralsight course entitled: Front-End First: Testing and Prototyping JavaScript Apps where I cover an introduction to Unit Testing, look at various examples of hard to test code and introduce the following libraries and tools... Mocha, Grunt, Mockjax, amplify.request, mockJSON, etc...

No comments:

Post a Comment