On Web apps, sometimes we want to avoid unnecessary requests, one of those situations happens when there is a form that represents a record, and the values of this record haven’t changed. On these cases you would like the user to send an update request only when the values of the record’s form actually changed, this technique is called “dirty form”, and is a very common practice.
I checked out some of the plugins out there to solve this problem, however, the functionality was sometimes sketchy. That’s why I started to implement my own solution, with a BDD approach using Screw.Unit.
In this post we will be covering some BDD practices, and at the same time I will be explaining what this plugins may offer to your plugin toolbox. Let’s start by pointing out the structure of a jQuery plugins project:
jquery-plugins/
|-- lib/
|-- spec/
`-- vendor/
In the lib/ folder we will have all the plugins we would like to develop, the spec/ folder will have all the test-related files and the vendor/ folder we will store all the frameworks needed in order to run the specs. In this project we will be using the livequery plugin, the Screw.Unit test framework and the Smoke mocking framework.
The spec/ folder will be composed (at least) by this two core files:
spec/
|-- suite.html
`-- spec_helper.js
The suite.html file will contain all the HTML scenarios needed for the jQuery plugins to work. This will look something like this (read the HTML comments):
<html>
<head>
<!-- first we import the jQuery framework -->
<script src="../vendor/screw-unit/lib/jquery-1.3.2.js"></script>
<!-- All vendor/ js files go here -->
<script src="../vendor/jquery.livequery.js"></script>
<!-- We import all the files needed by Screw.Unit -->
<script src="../vendor/screw-unit/lib/jquery.fn.js"></script>
<script src="../vendor/screw-unit/lib/jquery.print.js"></script>
<script src="../vendor/screw-unit/lib/screw.builder.js"></script>
<script src="../vendor/screw-unit/lib/screw.matchers.js"></script>
<script src="../vendor/screw-unit/lib/screw.events.js"></script>
<script src="../vendor/screw-unit/lib/screw.behaviors.js"></script>
<!-- We import all the files needed by Smoke -->
<script src="../vendor/smoke/lib/smoke.core.js"></script>
<script src="../vendor/smoke/lib/smoke.mock.js"></script>
<script src="../vendor/smoke/lib/smoke.stub.js"></script>
<script src="../vendor/smoke/plugins/screw.mocking.js"></script>
<!-- Here we include all our plugins from the lib/ folder -->
<script src="../lib/jquery.when_changed.js"></script>
<!-- We import all the spec files from the spec/ folder -->
<script src="spec_helper.js"></script>
<script src="jquery.when_changed_spec.js"></script>
<!-- We include the stylesheet from Screw.Unit to have nice looking test results -->
<link rel="stylesheet" href="../vendor/screw-unit/lib/screw.css">
<!--
We create a class scenario that will hold all the DOM needed for a test suite to work, but hides it from the
test HTML
-->
<style>
.scenario {
position: absolute;
left: -9999
}
</style>
</head>
<body>
<!-- This will be the place were all the test will grab the elements -->
<div id="sandbox" class="scenario"></div>
<!--
We will have clean states (fixtures) for the plugins, all of them will
have the postfix "-clean-state" to specify that this is a virgin state of a test environment
In this example, for each test case we execute, the "when-changed-clean-state" contents will be assigned to the
"sandbox" content.
-->
<div id="when-changed-clean-state" class="scenario">
<form method="GET">
<input type="text" name="text" value="some value">
<textarea name="textarea">some value</textarea>
<input type="checkbox" name="checkbox" value="1" checked>
<input type="radio" name="radio" class="first" value="one" checked>
<input type="radio" name="radio" class="second" value="two">
<select name="select">
<option value="1" class="one" selected>One</option>
<option value="2" class="two">Two</option>
</select>
</form>
</div>
</body>
</html>
The spec_helper.js file will have all the common setup for the test environment.
// This function well put the HTML contents of the clean-state we are working on to the sandbox
function resetDOM(id) {
$("#sandbox").html($("#" + id + "-clean-state").html());
}
// If we want to put some global setup and teardown code, here is the place to put it
// for setup, inside the before block
// for teardown, inside the after block
Screw.Unit(function() {
before(function() {
});
after(function(){
});
});
So, now that we have established our test environment, its time to do the implementation of the plugin, we start by defining the specs for it on the spec/jquery.when_changed_spec.js file, this file will represent the tests made to the lib/jquery.when_changed.js file.
Screw.Unit(function(){
describe("$.fn.whenChanged", function(){
// We establish a new clean state for each test case
// with a resetDOM invocation before each spec.
before(function(){
resetDOM("when-changed");
});
describe("invoked on an input:text", function(){
it("should invoke the given function when value has changed on keyup");
it("should not invoke the given function when value has not changed on keyup");
it("should not consider new blank text on the edges as a new text");
it("should call the reverse callback when the value returns to the same");
});
});
});
So, in the given spec file we specified (for now) the behavior of the plugin when we are interacting with an input text field. we expand this definitions by giving actual test code to each spec.
describe("invoked on an input:text", function(){
// first we define the setup for each spec, assign the plugin behavior to an input
before(function(){
// When the value of the input:text change it's original value, an alert will be called.
$("#sandbox input:text").whenChanged(function(){
window.alert("The input text has changed it's value");
});
});
it("should invoke the given function when value has changed on keyup", function(){
mock(window).should_receive("alert").exactly(1).with_arguments("The input text has changed it's value");
// the event checking will be raised by a keyup event
$("#sandbox input:text").val("some random value different than the original").trigger("keyup");
});
});
On the previous code, we are using the Smock’s method mock, this will do a temporary replacement of the window.alert function, and will check that this is being called exactly 1 time, and with the argument “The input text has changed it’s value”. This way we can check that the callback assigned on the whenChanged invocation was actually being called when necessary. Next step is to implement a whenChanged function that is simple enough to make the spec pass.
(function($){
$.fn.whenChanged = function(callback) {
// we store the old values on each element
$(this).each(function(){
$(this).data("jquery.whenChanged.oldValue", $(this).val());
});
// we assign a keyup event binding using livequery
$(this).livequery("keyup", function(){
var oldValue = $(this).data("jquery.whenChanged.oldValue");
var newValue = $(this).val();
if (oldValue !== newValue) {
callback.apply(this);
}
});
return this;
};
})(jQuery);
So we implemented the most simple solution possible that makes our specs run (Just check the suite.html file on a browser and you should have the feedback right away), now we continue developing adding more specs and changing the code as we go.
it("should not invoke the given function when value has not changed on keyup", function(){
mock(window).should_receive("alert").exactly(0);
$(this).trigger("keyup");
});
We added other method that didn’t need any code to be changed, in this case we are checking that window.alert is not being called at all. Awesome let’s continue with the next spec.
it("should not consider new blank text on the edges as a new text", function(){
mock(window).should_receive("alert").exactly(0);
$(this).val(" some value ").keyup();
});
At this point, this specs fails with our current implementation, we are not stripping the spaces on the border, this can be fixed easily.
$.fn.whenChanged = function(callback) {
$(this).each(function(){
// NOTICE: Invoking $.trim here!
$(this).data("jquery.whenChanged.oldValue", $.trim($(this).val()));
});
$(this).livequery("keyup", function(){
var oldValue = $(this).data("jquery.whenChanged.oldValue");
// NOTICE: Invoking $.trim here!
var newValue = $.trim($(this).val());
if (oldValue !== newValue) {
callback.apply(this);
}
});
return this;
};
Now that we have stripped the spaces on the old value storage, when we check the stripped new value, it will still be the same as the old value and the callback won’t be called. Finally we have to add the reverse callback functionality.
before(function(){
// When the value of the input:text change it's original value, an alert will be called.
$("#sandbox input:text").whenChanged(
function(){
window.alert("The input text has changed it's value");
},
function(){
window.alert("The input returned to the same value");
}
);
})
it("should call the reverse callback when the value returns to the same", function(){
mock(window).should_receive("alert").exactly(1).with_arguments("The input text has changed it's value");
$(this).val("Changing input value").keyup();
mock(window).should_receive("alert").exactly(1).with_arguments("The input returned to the same value");
$(this).val("some value").keyup();
});
So at this point, this spec will break previous specs, this is because the alert function is being called, even when the value is not changed, so mock(window).should_receive("alert").exactly(0) won’t work.
// NOTICE: we change this to alert with an specific argument.
it("should not invoke the given function when value has not changed on keyup", function(){
mock(window).should_receive("alert").exactly(0).with_arguments("The input text has changed it's value");
$(this).trigger("keyup");
});
it("should not consider new blank text on the edges as a new text", function(){
mock(window).should_receive("alert").exactly(0).with_arguments("The input text has changed it's value");
$(this).val(" some value ").keyup();
});
At the same time we need to add a new callback parameter to the whenChanged method.
$.fn.whenChanged = function(callback, reverseCallback) {
$(this).each(function(){
$(this).data("jquery.whenChanged.oldValue", $.trim($(this).val()));
});
// we assign a keyup event binding using livequery
$(this).livequery("keyup", function(){
var oldValue = $(this).data("jquery.whenChanged.oldValue");
var newValue = $.trim($(this).val());
if (oldValue !== newValue) {
callback.apply(this);
}
else {
reverseCallback.apply(this);
}
});
return this;
};
Now this code should run successfully and we now have covered our bases with input:text selectors, I could talk about how added support to other form input types, but I rather not in order to keep this short. Some thoughts I can share though is that the whenChanged method changed A LOT, after realizing that each type of input has it’s own gotchas, I created an specific whenChange method for each kind of form element. All in all… I was always sure that nothing broke on this process because the specs always told me if anything I was doing added any bug. Uhmmm the smell of good test code on late nights is incredible.
I hope this post can help you get some guidelines on how to get started with development of Javascript code using BDD, one word of caution though: testing effects in Javascript is a nasty challenge, you may like to pass an option to your method specifying if you want effects or not. check your code doesn’t depend on setTimeout invocations, take my word, is a BIG PAIN to test that.
Good luck with your BDD sessions.