Stub out a jQuery selector call?

22,173

Solution 1

The problem here is that $() is a function that returns an object with the method val(). So you have to stub $() to return a stubbed object having the method val.

$ = sinon.stub();
$.withArgs('#category').returns(sinon.stub({val: function(){}}));

But the main mistake here is to let the code you want to test call the function $() to create new instances. Why? Its best practice to create no new instances in your class, but to pass them into the constructor. Let's say you have function that will get a value out of an input, double it, and write it back to another:

function doubleIt(){
    $('#el2').val(('#el1').val() *2);
}

In this case you create 2 new objects by calling $(). Now you have to stub $() to return a mock and a stub. Using the next example you can avoid this:

function doubleIt(el1, el2){
    el2.val(el1.val() *2);
}

While, in the first case you have to stub $ to return a stub, in the second case you can easily pass a stub and a spy into your function.

So the sinon test for the second one would look like this:

var el1 =  sinon.stub({val: function(){}});
    el1.returns(2);

var el2 = sinon.spy({val: function(){}}, 'val')

doubleIt(el1, el2)

assert(el2.withArgs(4).calledOnce)

So, as you have no dom elements here, you can simply test your application logic with no need to create the same dom as in your app.

Solution 2

jQuery uses the css selector engine Sizzle under the hood and was decoupled so there's only a few places where it hooks in. You can intercept this to avoid any interaction with the dom.

jQuery.find is the important one to alter to respond with anything you want. Sinon could be used here or temporarily swapping the function out.

eg

existingEngine = jQuery.find
jQuery.find = function(selector){ console.log(selector) }
$(".test")
//>> ".test"
jQuery.find = existingEngine

you could also apply a specific catch condition with a fallback

existingEngine = jQuery.find
jQuery.find = function(selector){
  if(selector=='blah'}{ return "test"; }
  return existingEngine.find.apply(existingEngine, arguments)
}

In my recent work I have made a dummy object that responds like a dom node and wrapped that in a jQuery object. This will then respond to val() correctly and have all of the jquery methods present that it expects. In my case I'm simply pulling values from a form. If you are doing actual manipulation you might need to be more clever than this perhaps creating a temporary dom node with jQuery that represents what you expected.

obj = {
  value: "blah",
  type: "text",
  nodeName: "input",
}
$(obj).val(); // "blah"

Solution 3

Here is a pretty good guide to testing your views if you are using Backbone.js and Jasmin. Scroll down to the View section.

http://tinnedfruit.com/2011/04/26/testing-backbone-apps-with-jasmine-sinon-3.html

True, the stubs operate on objects. I guess the point of creating a view stub like so.

this.todoViewStub = sinon.stub(window, "TodoView")
        .returns(this.todoView);

Is just to be able to later render the view.

this.view.render();

In other words, append the '#category' div to the DOM of the testrunner, so that $ can act upon it. If your '#category' div is not in this.view, then you probably can just create a test.html page in which you run your isolated test. This is a common pattern in the Javascript MVC framework that I'm more used to that Backbone.

Here is a simple JMVC application structure example:

/todo
   /models
      todo.js
   /list
      /views
         init.tmpl
         listItem.tmpl
      list.css           
      list.js        (Controller)
      unitTest.js    (Tests for your list.)
      list_test.html (A html for your unit tests to run on.)

Having this setup you could just include the "#category" div in your list_test.html if you don't already happen to have it inside one of the views.

Share:
22,173

Related videos on Youtube

swilliams
Author by

swilliams

I enjoy Programming, Photography, Writing, Cooking, Running, and capitalizing the wrong letters in sentences.

Updated on July 09, 2022

Comments

  • swilliams
    swilliams almost 2 years

    I'm trying to get better at unit testing my JavaScript. I have the following code:

    var categoryVal = $('#category').val();
    if (categoryVal === '') { 
        doSomething();
    } 
    

    My test runner doesn't have the #category input on the page, so how would I stub/mock out the jQuery selector here? I've looked at both the jasmin and sinon documentation, but can't figure out how to get them to work here, since their stubs operate on objects, which $ is not.

    • pimvdb
      pimvdb over 12 years
      I've not used either of those libraries, but $ is an object in fact (functions are objects in JavaScript). Apart from that, jQuery functions do work when nothing is selected - .val() will just return undefined.
    • swilliams
      swilliams over 12 years
      I get that .val() will return undefined, but how can I then stub that to test my === ''?
  • swilliams
    swilliams over 12 years
    I ended up doing something like this. I refactored all of my DOM manipulations into separate functions, and stubbing the calls to those functions. Has the added benefit of keeping my methods extra svelte.
  • Phil
    Phil over 9 years
    This is too tightly coupled to jQuery internals. The unit test will break If jQuery changes the way it uses sizzle or ceases using it. Unit test only suppose to break if there is a bug in the code under test.
  • Timothy Gonzalez
    Timothy Gonzalez over 7 years
    Thank you for the detailed explanation of the issue. I'm having this problem in node js, but I get an error when I attempt to assign to $ saying $ isn't defined. I imagine this is from strict mode. Any idea how to get around this?
  • Jackie
    Jackie about 7 years
    Shouldn't there be a global in front of the $ in case of IIFE?
  • Srdjan Dejanovic
    Srdjan Dejanovic almost 7 years
    @Andreas Köberle, How you restore $? I added $.restore(); in the end of unit test and receive an error "$.restore is not a function". Do you know why?