Test is a JavaScript testing library that simulates your application functionality and verifies it's working correctly. You can do this by simulating DOM events and combination events like write and drag. You can also test your Ajax functionality by simulating Ajax requests. Then use assertions to check for correct behavior.
After reading this guide, you'll know how to get started using Test, how to simulate events and Ajax, how to test asynchronous functionality, and why you should run tests.
The place to include unit and functional tests. This file is only loaded in test mode. It is located at 'app/yourappname_test.js'
Used for testing lower level functionality, like a method or class.
Used for simulating user interactions, a level up from unit tests.
A subclass of functional tests designed to test JavaScriptMVC's controllers' actions.
Learn how to load the necessary files and write your first test.
Learn how to turn on test mode and load test files.
Change the src attribute of the script tag that loads include.js like this:
<script language="javascript" type="text/javascript" src="/jmvc/include.js?myapp,test"></script>
Reload your page. The JavaScriptMVC Console will load in another window:
The console provides helpful setup information. It states:
The application test file is where you'll load your unit, functional, and controller tests. This file is only loaded in test mode.
Create apps/myapp_test.js. Then reload your page.
Below are the steps to include a test. Instructions can also be found by clicking the unit tab in the console.
The console will display a message saying the test was successfully loaded. Now you're ready to create a test.
Place the following code in your myfirst_test.js file:
// creates a collection of unit tests called myfirst
new Test.Unit('myfirst',{
// create a test called truth
// only functions whose names begin with test_ are recognized as tests
test_truth: function() {
// make a trivial assertion that true is true
this.assert(true);
}
});
Now reload your page. Click the unit tab. You'll see a unit test. Click the play buttons or the test itself to run it.
To see this all in action, download JavaScriptMVC and turn on test mode. A test included with the download will be loaded, which you can run in the unit tab of the console.
The following sections cover the basics of Test, how to simulate your application's functionality, how to perform assertions, how to run your tests, and an overview of why testing is important.
For each type of testing, this section includes what it is used for, how to include it, and a usage example.
Unit tests are intended for the smallest testable part of your application, like methods and classes. For example, if you're using a date library that provides extra functionality for Date objects, you might want to create a date unit test collection.
To load a unit test put this in your application test file:
include.unit_tests('date');
Here's an example of a unit test called date_helpers. It contains tests that ensure humanize and to_date work correctly.
new Test.Unit('date_helpers',{
test_string_humanize: function(){
this.assert_equal('January 5th 2008', (new Date(2008,0,5)).humanize())
}, test_string_to_date: function(){
this.assert_equal(new Date(2008,0,5), 'January 5th 2008'.to_date());
}
});
Functional tests in JavaScriptMVC are for simulating user interactions, a level up from unit tests. For example, imagine an application that allows users to add events to a calendar. You'd create a calendar functional test, with tests creating, modifying, and deleting events through the user interface. Each test would simulate a user's interaction with the page.
Including a functional test is similar to including a unit test:
include.functional_tests('calendar');
To test adding an event in the calendar test example above, you'd simulate clicking the Add Event button, then assert the correct form appears, like this:
new Test.Functional('calendar',{
test_click_add_event: function(){
var params = this.Click('#new_event');
this.assert_equal('visible', $E('event_input'),style.display)
}
});
Controller tests are a special type of Functional test intended for use with JavaScriptMVC Controllers. Controller tests are supplied helper methods that simulate the DOM event that triggers each controller action.
An example will illustrate this point. In the following TodosController, the mouseover action is an event handler that is triggered whenever a mouseover occurs on an element with class todo. When the handler is called, the background color is changed to #8FBA3C.
Controller('todos',{
mouseover: function(params){
params.element.style.backgroundColor = '#8FBA3C';
}
});
When you create a Todos Test Controller, you're automatically be supplied with a TodosMouseover method. Calling this method will simulate mouseover event for the first element with class todo. Here's an example:
new Test.Controller('todos',{
test_mouseover: function(){
var params = this.TodoMouseover();
this.assert_equal('#8fba3c', params.element.style.backgroundColor);
}
});
To include this controller test, simply create a file in the 'test/functional' directory named todos_controller_test.js. No special include is necessary, as Test automatically attempts to load the corresponding test file for any controller that is loaded.
Testing boils down to two steps, repeated over and over:
Applying this to testing your web page's UI, this means:
In the code below, a mouseover for the todo element is tested:
test_mouseover : function(){
// simulate the mouseover event
// save the returned object, which contains the mousover event and todo element objects
var params = this.Mouseover('#todo');
// assert that the background color is #00ff00
this.assert_equal(params.element.style.backgroundColor, '#00ff00');
}
This section covers the correct way to name your test collections and the test functions.
When naming your functional and unit tests, the file name must match up correctly with the test collection name. For example, if you create a unit test called event, you'll name the file containing the test event_test.js (in the 'test/unit' directory), and you'll include the test with include.unit_tests('event'). The same goes for functional tests.
Controller tests should be named name to match the name of your controllers. For example, if you're testing the Todos Controller, the following are the naming conventions that must be followed:
Occasionally you'll find the need to create functions in your test collections that are not actually tests. Two reasons for doing so are:
Test functions and non-test functions are distinguished by whether the function name begins with test_. For example, test_click_event is a test, but assert_click_event is not. assert_click_event will not appear in the Console UI as a runnable test.
Simulating DOM events, like click and keypress, is the best way to test your JavaScript functionality. The following events can be simulated with Test:
change, click, contextmenu, dblclick, keyup, keydown, keypress, mousedown,
mousemove, mouseout, mouseover, mouseup, select, submit, dblclick, focus, blur
Call the event simulation methods within your tests like this:
this.Click('#my_button');
this.Mouseover('.todo',3);
this.Submit('#my_form');
this.Focus('#some_input');
this.Keypress('.todo input', {number: 2, character: 'D'});
You can expect the events to occur synchronously. Immediately after this.Keypress is invoked, the input element contains the given text.
The first parameter is a CSS selector string or an element object. The second parameter can be a number, which tells Test to perform the simulation on the nth item from the array of elements found. In the Mouseover example above, a mouseover event will be simulated for the 4th (0 counts as 1) element found with class todo. The second parameter can also be a hash of options, as in the Keypress example where letter 'D' is pressed for the 3rd todo input element.
High level events are commonly used sequences of DOM events. Test supports simulation of two high level events.
Write simulates writing in an input element. It can be called in the following way:
this.Write(input_params.element, 'Brian\n');
The first parameter is the element object or a CSS query string defining the input element to write in. The second parameter is the text to be written.
'\n' simulates pressing Enter, which usually triggers a form submit event. '\b' simulates pressing baskspace.
Calling write as done above causes the write to occur synchronously. The next line of code would run under the assumption the input element contains the correct text.
A second call signature causes the write to occur asynchronously, with a short delay between each keypress:
this.Write(input_params.element, {text: 'Brian', callback: this.next_callback()});
The use of next_callback causes the next test in the test collection to be called when the write is complete (more on this in Testing asynchronous functionality). By default, the callback is invoked after 0.5 seconds. If the write takes longer, this duration can be increased by using a duration option in the second parameter of Write.
Drag simulates dragging an element from one part of the page to another.
this.Drag($E('draggable'),{from: 'element1', to: 'element2',
duration: 2, callback: this.next_callback()});
Drag's use is similar to Write. To use it asynchronously, include a callback option. To use it synchronously, do not include a callback option. There are two additional parameters, from and to, describing the draggable element's source and destination coordinates.
Assertions are if statements that report their failure and success back to the Console window. The available assertions are assert, assert_equal, assert_null, and assert_not_null. Refer to the API for more information and examples.
Asynchronous functionality causes some action to occur in the background while the execution of the JavaScript function initiating the action continues. Loading Ajax data and Scriptaculous animations are two examples of asynchronous JavaScript functionality.
In order to test asynchronous functionality, you must trigger the event, wait for its completion, then use a callback function to verify the results are what you'd expect. In Test, this is done with the next and next_callback functions, which allow you to write clean and understandable code without nested functions
next is used to call the next function in the array after a certain delay. In the following example, a page element is double clicked, which causes an Ajax request to load a file. This will take time to complete, and you'll want to verify it happens correctly after the request returns:
test_open_directory: function(){
// double click the second directory, causing an Ajax file load
this.params = this.DirectoryDblclick(2);
// call the next function after a delay of 0.5 seconds (the default delay)
this.next();
},
// assume the file has been loaded when verify_open is called
verify_open: function(){
// check there are 5 files in the directory
this.assert_equal(5, params.element.childNodes.length);
}
next_callback is useful for calling a follow up function when callbacks are used. The following test simulates dragging an element.
test_drag: function(){
// initiate the 2 second drag
// call done_dragging after completion by passing the function name into next_callback
this.Drag($E('draggable'),{from: 'pointA', to: 'pointB',
duration: 2, callback: this.next_callback('done_dragging')})
},
done_dragging : function(){
// did the drag complete successfully
this.assert_equal(1, $E('pointB').next().childNodes.length);
}
The callback function is called with the original function's Test instance preserved. Therefore, its possible to save instance variables such as this.some_element = $('el'), and acccess the same variables in the callback.
You may need to set up your tests with sample data or clean up part of your environment after each test, such as by resetting the page. The optional setup and teardown methods can be written into your test collections for this purpose. They are called before and after each test. For example:
new Test.Unit('mytest',{
// called before every single test
setup: function(){
this.name = 'jimmy'
this.age = 150
},
// called after every single test
teardown: function(){
}
// our tests
test_1: function(){
this.assert(true);
}
test_2: function(){
}
});
The sequence of execution for this unit test is the following:
then...
Test makes it possible to simulate your AJAX functionality without involving a server. Instead of querying the server, your AJAX calls utilize sample data in fixture files.
The default Ajax is overwritten, and all AJAX calls are redirected to load files in the 'test/fixtures/your_application_name' directory. The file name is the request url with the request type appended as the suffix.
For example, if an application called 'myapp' makes an GET Ajax request to the url 'myrequest.xml', in test mode this would become a request for 'test/fixtures/myapp/myrequest.xml.get'. Place any XML or JSON test data inside this file, such as the following:
<data>two</data>
When you make a request to the same file a second or third time, the number of the request is added to the end of the file name. For example:
This allows you to maintain several versions of the same fake request data. This will come in handy any time you're changing data on the server, such as making a request for all todos after deleting or adding one.
Controller tests automatically create event simulation helpers for the tested controller's actions. For example, a Test.Controller that tests a controller like:
Controller('todos', {
// matches clicks on elements with className todo
click : function( params ){ ... },
// matches submits on elements with id todos
'# submit' : function(params) { ... }
});
will have helpers TodoClick and TodosSubmit:
new Test.Controller('todos', {
test_click : function(){
// clicks the 2nd element with className todo
this.TodoClick(2);
},
'# submit' : function(params) {
// submits the first element with id todos
this.TodosSubmit();
}
});
Pretty awesome! As it isn't possible to enumerate all possible helpers, they are displayed in the testing window for each test. You can click on each one to see it in action on the first matching element.
Each generated helper takes only the options_or_number parameter. If a number is provided, the event is performed on the nth element matched by the controller action. If an object is provided, it is passed to the event method it wraps, which is useful for keypress events.
JavaScriptMVC's Console window is where you run your tests. Unit tests are run from the Unit tab and functional and controller tests from the Functional tab. There are three ways to run your tests:
Controller action helpers are listed in the console interface under each collection of tests. Clicking these trigger their corresponding simulation, which is useful while creating tests. For example, clicking the FileClick link will cause the FileClick action to be triggered.
Testing provides you an automated way to know your application is behaving how you'd expect. As you develop features, you run your tests to make sure nothing has been accidentally broken.
The benefits of testing are the following:
Testing helps you find more errors, faster. Testing provides constant assurance your application works exactly how you expect. It costs exponentially less to catch a bug in testing than for a user to find it and subsequently choose to become someone else's user.
As you roll out new features, you'll run tests to make sure you aren't breaking old ones. That little voice telling you to be more careful? Good tests make him go away.
Automated testing is more reliable than human testing and frees you from the tedious task of running through tests on every browser. Solid testing increases developer productivity because you can run tests more quickly and catch problems as you're developing.
Overview
Test's highlights.
Demo
Testing a todo list built with Controller.
API
Low level documentation for Test's methods.