Skip to content

Instantly share code, notes, and snippets.

@jupiterjs
Created May 28, 2011 02:47
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jupiterjs/996538 to your computer and use it in GitHub Desktop.
Save jupiterjs/996538 to your computer and use it in GitHub Desktop.
FuncUnit article

FuncUnit

Testing is an often overlooked part of front end development. Most functional testing solutions are hard to set up, expensive, use a difficult (non JavaScript) API, are too hard to debug, and are don't accurately simulate events. FuncUnit, JavaScriptMVC's testing solution, is designed to solve all these problems. No setup, firebug debugging, a jQuery-like API, and the most accurate possible event simulation make FuncUnit a comprehensive testing solution.

Overview

FuncUnit is a collection of several components:

  • jQuery - for querying elements and testing their conditions
  • QUnit - jQuery's unit test framework for setting up tests
  • Syn - an event simulation library made for FuncUnit that simulates clicks, types, drags
  • Selenium - used to programatically open and close browsers

FuncUnit tests are written in JavaScript, with an API that looks identical to jQuery. To run them, you open a web page that loads your test. It opens your application in another page, runs your test, and shows the results.

Alternatively, the same test will run from the command line, via Selenium. You run a command (or automate this in your continuous integration system), which launches a browser, runs your tests, and reports results.

Getting Set Up

Getting started with writing a FuncUnit test involves:

  1. Creating a funcunit.html page that loads a test script. The tests run within the QUnit framework, so the page needs some elements required by QUnit.

     <head>
         <link rel="stylesheet" type="text/css" href="funcunit/qunit/qunit.css" />
         <title>FuncUnit Test</title>
     </head>
     <body>
         <h1 id="qunit-header">funcunit Test Suite</h1>
         <h2 id="qunit-banner"></h2>
         <div id="qunit-testrunner-toolbar"></div>
         <h2 id="qunit-userAgent"></h2>
         <ol id="qunit-tests"></ol>
         <script type='text/javascript' src='steal/steal.js?steal[app]=myapp/test/funcunit/mytest.js'></script>
     </body>
    
  2. Create the test script. Steal funcunit, use QUnit's setup method to set your test up, and the test method to write your test.

    steal.plugins('funcunit').then(function(){ module("yourapp test", { setup: function(){ S.open("//path/to/your/page.html"); } }); test("First Test", function(){ ok(true, "test passed"); }); })

You should now have a working basic FuncUnit test. Open the page and see it run.

Writing a Test

Writing FuncUnit tests involves a similar repeated pattern:

  1. Opening a page (only do this once in the setup method).
  2. Perform some action (click a link, type into an input, drag an element)
  3. Wait for some condition to be true (an element becomes visible, an element's height reaches a certain value) 3*. Check conditions of your page (does the offset of a menu element equal some expected value).
  • This can be done implicitly in step 2. If the conditions of your wait don't become true, the test will never complete and will fail.

This pattern breaks down into the three types of methods in the FuncUnit API: actions, waits, and getters.

Most commands (except open) follow the pattern of being called on a FuncUnit object, similar to a jQuery.fn method. The S method is similar to $, except it doesn't return a jQuery object. It accepts any valid jQuery selector and the results are chainable:

S("a.myel").click().type("one").visible();

QUnit Methods

If you're familiar with QUnit, this section will be review. FuncUnit adds its own API on top of the QUnit framework.

Module is the method used to define a collection of tests. It accepts a module name, and an object containing a setup method (called before each test), and a teardown method (called after each test).

module("module name", { 
    setup: function(){}
    teardown: function(){}	
});

Test is the method used to define each test. Ok and equals are the two most common assertions. If their conditions are true, the assertion will pass. If any assertion fails, the test fails.

Action Commands

Actions include open, click, dblclick, rightclick, type, move, drag, and scroll.

S("#foo").click();
S("#foo").type("one");
S("#foo").drag("#bar");

They use the syn library to accurately simulate the event exactly as the browser would process it from a real user. Click causes the correct sequence of browser events in each browser (mousedown, click, mouseup, focus if its a form element).

Note: Action commands don't actually get processed synchronously. This means you can't set a breakpoint on an action and step through the statements one by one. Each action command is asynchronous, so in order to prevent crazy nested callbacks everywhere, the FuncUnit API will add action commands to a queue, calling each after the previously queued item has completed.

Wait Commands

Waits are used to make your tests wait for some condition in the page to be true before proceeding. Waits are needed after almost every action to prevent your tests from being brittle. If you don't use a wait, you're assuming the action's results are instantaneous, which often, they aren't.

Waits correspond to jQuery methods of the same name.

Dimensions - width, height Attributes - attr, hasClass, val, text, html Position - position, offset, scrollLeft, scrollTop Selector - size, exists, missing Style - css, visible, invisible

Waits, like actions, are asynchronous commands, and add themselves to the FuncUnit command queue, so they can't be inspected with breakpoints.

Each wait accepts the attribute you'd expect it to to wait for its condition to be true. For example, width accepts a single number (the width to wait for). HasClass accepts a class string. Css accepts a property and its value.

You can also pass a function instead of a wait value. This function will be called over and over until it is true, and then the wait is complete.

S(".foo").width(10);       // wait for foo to be 10px
S(".foo").hasClass("bar")  // wait for foo to have class "bar"
S(".foo").visible()        // wait for foo to be visible

Getters

Getters are used to test conditions of the page, usually within an assertion. They are actually the exact same method names as waits, listed above, but called without a wait condition.

var width = S(".foo").width(); var text = S(".foo").text();

Every action and wait command accepts an optional callback function, which is called after the method completes. In these callbacks, you'd test page conditions with getters and do assertions.

S(".foo").visible(function(){
    ok(S(".bar").size(), 5, "there are 5 bars");
});

You can set breakpoints inside these callbacks, test page conditions, and debug assertions that are failing.

A Real Test

Here's a test for an autocomplete widget.

module("autosuggest",{
  setup: function() {
    S.open('autosuggest.html')
  }
});

test("JavaScript results",function(){
  S('input').click().type("JavaScript")

  // wait until we have some results
  S('.autocomplete_item').visible(function(){
    equal( S('.autocomplete_item').size(), 5, "there are 5 results")
  })
});

Running Tests

There are two ways to run tests: browser and command line. To run from browser, just open your test page in the browser. Devs can use this while developing and debugging.

To run from command line, open your test with funcunit/envjs.

funcunit/envjs myapp/funcunit.html

You can configure Selenium options (like browsers) in myapp/settings.js.

FuncUnit = { browsers: ["*firefox", "*iexplore", "*safari", "*googlechrome"] };

Special Events and Dom Extensions

JavaScriptMVC is packed with jQuery helpers that make building a jQuery app easier and fast. Here's the some of the most useful plugins:

CurStyles

Rapidly retrieve multiple css styles on a single element:

$('#foo').curStyles('paddingTop',
                    'paddingBottom',
                    'marginTop',
                    'marginBottom');

Fixtures

Often you need to start building JS functionality before the server code is ready. Fixtures simulate Ajax responses. They let you make Ajax requests and get data back. Use them by mapping request from one url to another url:

$.fixture("/todos.json","/fixtures/todos.json")

And then make a request like normal:

$.get("/todos.json",{}, function(){},'json')

Drag Drop Events

Hover

Default

Destroyed

Hashchange

Building a todo list

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment