Mock-Testing Backbone with Sinon

To dip our toes into testing, we used Intern in Node mode. But to test the browser side of our application, we need to run our tests in a browser environment. (Well, “need” might be too strong a word. Let’s just say that testing a browser-based application in a nonbrowser environment is less than ideal.) Yet we still want the ease and automatability of command-line testing. What to do?

The answer is Selenium, a project that allows browsers to be controlled from the command line. Selenium can run on virtually any system and talk to any major browser. In fact, several providers are available to sell Selenium as a service. Rather than running Selenium on your local machine, which would limit you to the browsers and the OS you have installed, you can run your tests in the cloud on every possible browser and OS combination you want to support.

For the remainder of this chapter, I’ll be running my browser-based tests with a remote Selenium service called Sauce Labs.[54] They offer a fourteen-day free trial with no credit card required. In addition to versatility, running Selenium remotely is a lot less of a hassle. However, if you’d prefer not to use a remote testing provider, you can download Selenium Server for yourself.[55]

For our UI unit tests, let’s create a new directory:

 $ ​​mkdir​​ ​​tests/src/ui_unit

And let’s add a new task to our Grunt config. In addition to pointing to an Intern config file in the new directory (which we’ll create momentarily), we specify runType: ’runner’ instead of runType: ’client’, meaning that the tests will run in a Selenium runner instead of a Node client:

 ui_unit:
  options:
  runType: ​'runner'
  config: ​'tests/compiled/ui_unit/intern'
  reporters: [​'console'​]

Now let’s create the Intern config for our new test directory, pointing at one test suite for each of our app’s three Backbone entities:

 define
  excludeInstrumentation: ​/^(?:tests|bower_components)//
  loader:
  packages: [
  {name: ​'sinon'​, location: ​'bower_components/sinon/lib'​}
  {name: ​'jquery'​, location: ​'bower_components/jquery/dist'​}
  {name: ​'underscore'​, location: ​'bower_components/underscore'​}
  {name: ​'backbone'​, location: ​'bower_components/backbone'​}
  {name: ​'app'​, location: ​'assets/coffee'​}
  ]
  suites: [
 'tests/compiled/ui_unit/board'
 'tests/compiled/ui_unit/column'
 'tests/compiled/ui_unit/card'
  ]
  environments: [
  {
  browserName: ​'chrome'
  version: [​'38'​]
  platform: [​'Linux'​]
  }
  ]
 # Add your own tunnel here, e.g.
 # tunnel: 'SauceLabsTunnel'
 # tunnelOptions: {
 # username: 'my-username'
 # accessKey: 'ea961239-0c3c-c3ab-715c-99de41defaa8'
 # }

There’s a lot more going on here than there was in our first Intern config. First of all, there’s excludeInstrumentation. This regular expression tells Intern to ignore any scripts whose path contains tests or bower_components when computing code coverage statistics, ensuring that those statistics only pertain to our application code.

The packages map given to loader (referring to Intern’s AMD module loader) tells it where to look for various scripts that need to be loaded in the test suites. I like to keep this information in the Intern config rather than having to repeat all of these paths across every test suite.

As in our first Intern config, suites points Intern to the test suites we’ll be compiling.

Now here’s where things get interesting: environments tells Intern what kind of virtual machine to request from our test environment provider (e.g. Sauce Labs). You can provide multiple environments here to run all of your tests in each of several browsers. For now, I’m keeping things simple by just requesting a recent version of Chrome under Linux.

Finally, you’ll need to un-comment the tunnel and tunnelOptions lines and enter your test environment provider credentials, unless you’re running Selenium locally. For more information on configuring Intern, the docs are in the wiki: https://github.com/theintern/intern/wiki/Configuring-Intern

Now to write our first unit test! Let’s start with our simplest entity, Card. The Card model has no functionality of its own, beyond what it gets by subclassing Backbone.Model. CardCollection, however, has to talk to the server to fetch data. So we should write a test to confirm that the collection populates as expected after we call fetch.

We could run a remote server for the test browser to talk to, but if we did that, our tests could fail for any number of reasons (server failure, network failure, server and browser code out of sync). We want to be sure that test failures indicate a problem with our browser code only. So instead, we’ll use “mock” Ajax: any time our app would send an Ajax request, we’ll intercept it and dictate the response in our test code. To do that, we’ll use a library called Sinon.[56]

Let’s start by installing Sinon with Bower:

 $ ​​bower​​ ​​install​​ ​​--save-dev​​ ​​sinon

We need to tell Intern to load Sinon into the browser. Intern uses an AMD (Asynchronous Module Definition[57]) loader. The good news is that Sinon supports AMD. The bad news is that it supports it in an unusual way, where you have to individually load each part of Sinon you need. So, to avoid repeating all of those declarations across every test suite, let’s create a utility file:

 define [
 'intern/order!sinon/sinon'
 'intern/order!sinon/sinon/spy'
 'intern/order!sinon/sinon/call'
 'intern/order!sinon/sinon/behavior'
 'intern/order!sinon/sinon/stub'
 'intern/order!sinon/sinon/mock'
 'intern/order!sinon/sinon/collection'
 'intern/order!sinon/sinon/assert'
 'intern/order!sinon/sinon/sandbox'
 'intern/order!sinon/sinon/test'
 'intern/order!sinon/sinon/test_case'
 'intern/order!sinon/sinon/match'
 'intern/order!sinon/sinon/util/event'
 'intern/order!sinon/sinon/util/fake_xml_http_request'
 ], (sinon) ->
 
  sinon

Whew! As you can see, Sinon has quite a few features to offer. But what exactly do those strings mean?

Anything before a ! in an AMD loader string is a plugin name. That means that the plugin takes the module specified by the rest of the string and performs some operation on it. In this case, we’re using the intern/order plugin. Its effect is very simple: it ensures that the referenced scripts run in the order we’ve specified. This is important for Sinon, because the later scripts try to attach objects to the global sinon object defined by the first script.

The sinon/ at the start of the module path proper points to the path that we referenced in our Intern config’s packages section. The rest is just the path of a .js file. We’ll continue to use this idiom for every component we load from outside of Intern.

In our actual test module, we need to load not only Sinon, but also—and arguably more importantly—the entities we’re testing. Also, since those entities rely on Backbone, we’ll have to bring Backbone into the test. Backbone, in turn, expects Underscore and jQuery to be defined. So we bring all of those, using the intern/order plugin to preserve the order in which the modules need to be loaded into the browser:

 define [
 'intern!object'
 'intern/chai!assert'
 './utils/sinon'
 'intern/order!jquery/jquery'
 'intern/order!underscore/underscore'
 'intern/order!backbone/backbone'
 'intern/order!app/card'
 ], (registerSuite, assert, sinon) ->

For our first real unit test, let’s mock an empty response and confirm that the collection contains no cards:

 fakeXHR = null
 responseHeaders = {​'Content-type'​: ​'application/json'​}
 
 startFakeXHR = ->
  fakeXHR = sinon.useFakeXMLHttpRequest()
  fakeXHR.requests = []
  fakeXHR.onCreate = (req) ->
  fakeXHR.requests.push(req)
 
 stopFakeXHR = ->
  fakeXHR.restore()
 
 registerSuite
  name: ​'CardCollection'
 
 'has no cards after empty response from /cards'​: ->
  startFakeXHR()
  cards = ​new​ window.CardCollection()
 
  fetchPromise = cards.fetch()
  assert.equal(requests[0].url, ​'/cards'​)
  requests[0].respond(200, responseHeaders, JSON.stringify([]))
 
 # Return this promise so Intern knows when the test is over
  fetchPromise.then =>
  assert.strictEqual cards.length, 0
  stopFakeXHR()

This bears some explaining. What’s with startFakeXHR and stopFakeXHR? Well, when we call Sinon’s useFakeXMLHttpRequest, we override the global XMLHttpRequest function. Unfortunately, Intern uses that same function to communicate with us! (We’re running this test on a remote server, remember?) So we have to restore the browser’s XHR functionality right away.

There’s also the matter of timing. By default, Intern assumes that a test is done as soon as you return from it; it reports the result and moves on. But a fetch (even a mocked one) is asynchronous—the collection won’t be populated until after the test function returns. So we have to return a promise representing the completion of the test. The one returned by fetch().then will do nicely. If you’re not familiar with promises, Sandeep Panda has provided a good overview.[58]

And now let’s mock a slightly more interesting response:

 'has 2 cards after 2-length response from /cards'​: ->
  startFakeXHR()
  cards = ​new​ window.CardCollection()
 
  fetchPromise = cards.fetch()
  assert.equal(requests[0].url, ​'/cards'​)
  requests[0].respond(200, responseHeaders, JSON.stringify([
  {id: ​'1'​, description: ​'First'​}
  {id: ​'2'​, description: ​'Second'​}
  ]))
 
 # Return this promise so Intern knows when the test is over
  fetchPromise.then =>
  assert.strictEqual cards.length, 2
  stopFakeXHR()

Excellent! Now if we just do the same for Column and Board, we’ll have all of the parts of our front end that talk directly to the server covered. Speaking of the server...

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.29.119