Reputation: 5922
I have code in a JS file that like this (simplified of course):
$(function () {
var num;
$.getJSON('./getNumber.php', function (n) {
num = n;
});
$('#id').on('click', function () { alert(num); });
});
I need to write two unit tests:
./getNumber.php
request to server when page loads#id
is clicked, the num
gets alerted.Obviously I need FakeXmlHTTPRequest and mock the alert
function, maybe even spy the $.getJSON
. However I am not sure what's the right way to write the tests while keeping the two atomic.
I think the only to doing so would dynamic inject the <script>
block for each test; but I just feel that's not right. What's the right way? Thanks.
Edit: based on the comments outside of SO, what I need to learn is to write testable Javascript, instead of trying to come up test cases for something with low testabililty. If anyone can give me some advice on rewriting this code it will be also greatly appreciated.
Upvotes: 3
Views: 761
Reputation: 12769
There isn't one right answer to your question, but rather a lot of good principles to learn from. Consider the following code. I've refactored your example. I'll explain everything I did below.
// Define your module in a self-executing function.
// This module is now completely unit testable.
var myModule = (function () {
var num;
function getNum() {
$.getJSON('./getNumber.php', function (n) {
num = n;
});
}
function alertNum() {
alert(num);
}
// this returned object will by set to myModule and will publicly
// expose the necessary functions
return {
getNum: getNum,
alertNum: alertNum
};
})();
// wire up your events
$(function() {
$("#id").on('click', myModule.alertNum);
});
This example is obviously overkill for this simple code, but it should give you some good ideas.
Your core functionality is now encapsulated in a module. You keep additional private functions and variables inside the module (like num
), but everything publicly exposed through the returned object can now be tested. (See Note below)
I've wired up the events outside the module. If your module is large, you may even want to put this code in another script. Either way, it separates your concerns in a more MVC-like approach. You can test this chunk of code, but you shouldn't really have to; it should be very straightforward jQuery, and wrapping tests around it would be little more than testing the jQuery library, which has already been thoroughly tested by the jQuery team.
This may seem weird, but don't use any jQuery selectors inside your module! If your module needs an element, pass it in as a function parameter. There are ways to stub that stuff out, but it's really a hassle, and shouldn't be necessary if you've properly separated your concerns. Most of my modules (or the objects in them) wind up with an init()
method where I can pass in stuff. -- You might still choose to use jQuery's .find()
method on your elements inside your module. That's more a judgement call only you can make based on your module's particular needs. Doing so will require you to do more detailed mocks of DOM elements in your tests, but sometimes it's just easier than passing in dozens of elements into init()
(just pass in the main container instead).
Note: I strongly recommend using RequireJS or another AMD module loader for managing your modules. This will keep things like myModule
above out of the global namespace. Setting up Require with QUnit is a little persnickety, though, and might be a problem to tackle later once you get a handle on unit testing.
Upvotes: 2