Create the test suite

Một phần của tài liệu single page web applications (Trang 400 - 417)

As of chapter 6, we have all the ingredients for successful testing of our Model using known data (thanks to the Fake module) and a well-defined API. Figure B.5 shows how we plan to test the Model:5

Before we can start testing, we need to get Node.js to load our modules. Let’s do that next.

B.4.1 Get Node.js to load our modules

Node.js handles global variables differently than browsers. Unlike browser JavaScript, variables in a file are local by default. Effectively, Node.js wraps all library files in an

Listing B.3 Installing jQuery and nodeunit system-wide

4 For a currently running session, type exportPATH=/usr/lib/node_modules. Depending on how Node.js is installed, the path may vary. On Mac, you might try /usr/local/share/npm/lib/node_modules.

5 The astute interloper will notice that this figure is a lazy, pixel-perfect copy of one presented earlier. We should get paid by the column-inch...

Figure B.5 Testing the Model using the test suite and fake data (mode 1)

anonymous function. The way we make a variable available across all modules is to make it a property of the top-level object. And the-top level object in Node.js is not window, like it is in browsers, but is instead called—wait for it—global.

Our modules are designed for use by the browser. But with ingenuity, we can have Node.js use them with little modification. Here’s how we do it: our entire application runs in the single namespace (object) of spa. So if we declare a global.spa attribute in our Node.js test script before we load our modules, everything should work as expected.

Now before all that evaporates from our short-term memories, let’s start our test suite, webapp/public/nodeunit_suite.js, as shown in the following listing.

/*

* nodeunit_suite.js * Unit test suite for SPA *

* Please run using /nodeunit <this_file>/

*/

/*jslint node : true, continue : true, devel : true, indent : 2, maxerr : 50, newcap : true, nomen : true, plusplus : true, regexp : true, sloppy : true, vars : false, white : true

*/

/*global spa */

// our modules and globals global.spa = null;

We only need to adjust the root JavaScript file (webapp/public/js/spa.js) to finish loading our modules. Our adjustment allows the test suite to use the correct global spa variable as shown in the next listing. Changes are shown in bold:

/*

* spa.js

* Root namespace module

*/

...

/*global $, spa:true */

spa = (function () { 'use strict';

var initModule = function ( $container ) { spa.data.initModule();

spa.model.initModule();

if ( spa.shell && $container ) { spa.shell.initModule( $container );

} };

return { initModule: initModule };

}());

Listing B.4 Declare our namespace in the test suite—webapp/public/nodeunit_suite.js

Listing B.5 Adjust our root SPA JavaScript—webapp/public/js/spa.js

Add the node:true switch to have JSLint assume the Node.js environment.

Create a global.spa attribute so the SPA modules can use the spa namespace when they load.

Add spa:true to the configuration so that JSLint will allow us to assign to the spa global variable.

Remove the var declaration.

Adjust the application so that it can run without the user interface (the Shell).

377 Create the test suite

Now that we’ve created a global.spa variable, we can load our modules much like we did with our browser document (webapp/public/spa.html). First we’ll load our third-party modules like jQuery and TaffyDB, and make sure their global variables are also available (jQuery, $, and TAFFY, if you must know). Then we can load our jQuery plugins and then our SPA modules. We won’t load our Shell or feature modules, because we don’t need them to test the Model. Let’s update our unit test file while these thoughts still linger in our consciousness. Changes are shown in bold:

...

/*global $, spa */

// third-party modules and globals global.jQuery = require( 'jquery' );

global.TAFFY = require( './js/jq/taffydb-2.6.2.js' ).taffy;

global.$ = global.jQuery;

require( './js/jq/jquery.event.gevent-0.1.9.js' );

// our modules and globals global.spa = null;

require( './js/spa.js' );

require( './js/spa.util.js' );

require( './js/spa.fake.js' );

require( './js/spa.data.js' );

require( './js/spa.model.js' );

// example code spa.initModule();

spa.model.setDataMode( 'fake' );

var $t = $( '<div/>' );

$.gevent.subscribe(

$t, 'spa-login',

function ( event, user ){

console.log( 'Login user is:', user );

} );

spa.model.people.login( 'Fred' );

Whoops, we got ambitious and snuck in a short test script at the end of our listing.

Although we eventually want to use nodeunit to run this file, we’ll use Node.js to run it first to ensure it’s loading the libraries properly. Indeed, when we run our test suite using Node.js we see something like this:

$ node nodeunit_suite.js Login user is: { cid: 'id_5',

name: 'Fred',

css_map: { top: 25, left: 25, 'background-color': '#8f8' }, ___id: 'T000002R000003',

___s: true, id: 'id_5' }

Listing B.6 Adding the libraries and our modules—webapp/public/nodeunit_suite.js

If you’re playing along at home, please be patient. It takes three seconds before we see any output because the Fake module pauses that long before completing a sign-in request. And it takes another eight seconds after the output for Node.js to finish run- ning. That’s because Fake module uses timers when emulating the server (timers are created by the setTimeout and setInterval methods). Until those timers are com- plete, Node.js considers the program “running” and doesn’t exit. We’ll come back to this issue later. Now let’s get familiar with nodeunit.

B.4.2 Set up a single nodeunit test

Now that we have Node.js loading our libraries, we can focus on setting up our node- unit tests. First let’s get comfortable with nodeunit all by itself. The steps to running a successful test are as follows:

■ Declare the test functions.

■ In each test function, tell the test object how many assertions to expect using test.expect(<count>).

■ In each test, run the assertions; for example test.ok(true);.

■ At the end of each test, tell the test object that this test is complete using test.done().

■ Export the list of tests to be run in order. Each test will be run only after the prior test is complete.

■ Run the test suite using nodeunit <filename>.

Listing B.7 shows a nodeunit script using these steps for a single test. Please read the annotations as they provide helpful insight:

/*jslint node : true, sloppy : true, white : true */

// A trivial nodeunit example // Begin /testAcct/

var testAcct = function ( test ) { test.expect( 1 );

test.ok( true, 'this passes' );

test.done();

};

// End /testAcct/

module.exports = { testAcct : testAcct };

When we run nodeunit nodeunit_test.js we should see the following output:

$ nodeunit_test.js

✔ testAcct

OK: 1 assertions (3ms)

Listing B.7 Our first nodeunit test—webapp/public/nodeunit_test.js

Declare a test function called testAcct. We can name a test whatever we want; it just has to be a function that takes a test object as its only argument.

Tell the test object that we plan to run a single assertion.

Invoke our first (and only) assertion in this example.

Invoke test.done() so that nodeunit may proceed to the next test (or exit).

Export our tests in the order we want

nodeunit to run them.

379 Create the test suite

Now let’s combine our nodeunit experience with the code we want tested.

B.4.3 Create our first real test

We’ll now convert our first example into a real test. We can use nodeunit and jQuery deferred objects to avoid the pitfalls of testing event-driven code. First, we rely on the fact that nodeunit won’t proceed to a new test until the prior test declares it’s finished by executing test.done(). This makes testing easier to write and understand. Second, we can use a deferred object in jQuery to invoke test.done() only after the required spa-login event has been published. This then allows the script to proceed to the next test. Let’s update our test suite as shown in listing B.8. Changes are shown in bold:

...

// our modules and globals global.spa = null;

require( './js/spa.js' );

require( './js/spa.util.js' );

require( './js/spa.fake.js' );

require( './js/spa.data.js' );

require( './js/spa.model.js' );

// Begin /testAcct/ initialize and login var testAcct = function ( test ) { var $t, test_str, user, on_login, $defer = $.Deferred();

// set expected test count test.expect( 1 );

// define handler for 'spa-login' event on_login = function (){ $defer.resolve(); };

// initialize

spa.initModule( null );

spa.model.setDataMode( 'fake' );

// create a jQuery object and subscribe $t = $('<div/>');

$.gevent.subscribe( $t, 'spa-login', on_login );

spa.model.people.login( 'Fred' );

// confirm user is no longer anonymous user = spa.model.people.get_user();

test_str = 'user is no longer anonymous';

test.ok( ! user.get_is_anon(), test_str );

// declare finished once sign-in is complete $defer.done( test.done );

};

// End /testAcct/ initial setup and login module.exports = { testAcct : testAcct };

Listing B.8 Our first real test—webapp/public/nodeunit_suite.js

When we run the test suite using nodeunit./nodeunit_suite.js we should see the following output:

$ nodeunit nodeunit_test.js

✔ testAcct

OK: 1 assertions (3320ms)

Now that we’ve successfully implemented a single test, let’s map out the tests we want to have in our suite and discuss how we’ll ensure they execute in the correct sequence.

B.4.4 Map the events and tests

When we tested the Model manually in chapters 5 and 6, waiting for some process to complete before typing in the next test came naturally. It’s obvious to humans that we must wait for sign-in to complete before we can test messaging. But this isn’t obvious to a test suite.

We must map out a sequence of events and tests for our test suite to work. One benefit of writing test suites is that it makes us analyze and understand our code more completely. Sometimes we find more bugs when writing tests than when run- ning them.

Let’s first design a test plan for our suite. We want to test the Model as our imagi- nary user, Fred, puts our SPA through its paces. Here is what we’d like Fred to do, with labels:

■ testInitialState—Test the initial state of the Model.

■ loginAsFred—Sign in as Fred and test the user object before the process completes.

■ testUserAndPeople—Test the online-user list and the user details.

■ testWilmaMsg—Receive a message from Wilma and test the message details.

■ sendPebblesMsg—Change the chatee to Pebbles and send her a message.

■ testMsgToPebbles—Test the content of the message sent to Pebbles.

■ testPebblesResponse—Test the content of a response message sent by Pebbles.

■ updatePebblesAvtr—Update data for Pebbles’ avatar.

■ testPebblesAvtr—Test the update of Pebbles’ avatar.

■ logoutAsFred—Sign out as Fred.

■ testLogoutState—Test the state of the Model after sign-out.

Our test framework, nodeunit, runs tests in the order presented, and won’t proceed to the next test until the prior test has declared that it has finished. This works to our advantage, as we want to ensure that specific events have occurred before certain tests are run. For example, we want a user sign-in event to occur before we test the online person list. Let’s map out our test plan with the events that need to occur before we can proceed from each test, as shown in listing B.9. Note that our test names match the labels from our plan exactly, and they’re human-readable:

381 Create the test suite

// Begin /testInitialState/

// initialize our SPA

// test the user in the initial state // test the list of online persons // proceed to next test without blocking // End /testInitialState/

// Begin /loginAsFred/

// login as 'Fred'

// test user attributes before login completes

// proceed to next test when both conditions are met:

// + login is complete (spa-login event)

// + the list of online persons has been updated // (spa-listchange event)

// End /loginAsFred/

// Begin /testUserAndPeople/

// test user attributes

// test the list of online persons

// proceed to next test when both conditions are met:

// + first message has been received (spa-updatechat event) // (this is the example message from 'Wilma')

// + chatee change has occurred (spa-setchatee event) // End /testUserAndPeople/

// Begin /testWilmaMsg/

// test message received from 'Wilma' // test chatee attributes

// proceed to next test without blocking // End /testWilmaMsg/

// Begin /sendPebblesMsg/

// set_chatee to 'Pebbles' // send_msg to 'Pebbles' // test get_chatee() results

// proceed to next test when both conditions are met:

// + chatee has been set (spa-setchatee event) // + message has been sent (spa-updatechat event) // End /sendPebblesMsg/

// Begin /testMsgToPebbles/

// test the chatee attributes // test the message sent

// proceed to the next test when

// + A response has been received from 'Pebbles' // (spa-updatechat event)

// End /testMsgToPebbles/

// Begin /testPebblesResponse/

// test the message received from 'Pebbles' // proceed to next test without blocking // End /testPebblesResponse/

// Begin /updatePebblesAvtr/

// invoke the update_avatar method // proceed to the next test when

Listing B.9 Test plan with blocking events detailed

// + the list of online persons has been updated // (spa-listchange event)

// End /updatePebblesAvtr/

// Begin /testPebblesAvtr/

// get 'Pebbles' person object using get_chatee method // test avatar details for 'Pebbles'

// proceed to next test without blocking // End /testPebblesAvtr/

// Begin /logoutAsFred/

// logout as fred

// proceed to next test when

// + logout is complete (spa-logout event) // End /logoutAsFred/

// Begin /testLogoutState/

// test the list of online persons // test user attributes

// proceed without blocking // End /testLogoutState/

This plan is linear and easy to understand. In the next section, we’ll put our plan into practice.

B.4.5 Create the test suite

We can now add some utilities and incrementally add tests to our suite. At each step we’ll run the suite to check our progress.

ADDTESTSFORINITIALSTATEANDSIGN-IN

We’ll begin our test suite by writing some utilities and adding our first three tests to check the initial Model state, have Fred sign in, and then check the user and person list attributes. We’ve found that tests typically fall into two categories:

1 Validation tests where many assertions (like user.name ==='Fred' ) are used to check the correctness of program data. These tests often don’t block.

2 Control tests that perform actions like signing in, sending a message, or updat- ing an avatar. These tests rarely have many assertions and often block progress until an event-based condition is met.

We’ve found it is best to embrace this natural division, and we name our tests accord- ingly. Validation tests are named test<something>, and the control tests are named after what they do, like loginAsFred.

The loginAsFred test requires that the sign-in be complete and the list of online users be updated before allowing nodeunit to proceed to the testUserAndPeople test.

This is accomplished by having the $t jQuery collection subscribe handlers for the spa-login and spa-listchange events. The test suite then uses jQuery deferred objects to ensure these events occur before loginAsFred executes test.done().

Let’s update the test suite as shown in listing B.10. As always, please read the anno- tations as they provide additional insight. The comments we built for our test plan in listing B.9 are shown in bold:

383 Create the test suite

...

/*global $, spa */

// third-party modules and globals ...

// our modules and globals ...

var

// utility and handlers

makePeopleStr, onLogin, onListchange, // test functions

testInitialState, loginAsFred, testUserAndPeople, // event handlers

loginEvent, changeEvent, loginData, changeData, // indexes

changeIdx = 0, // deferred objects

$deferLogin = $.Deferred(), $deferChangeList = [ $.Deferred() ];

// utility to make a string of online person names makePeopleStr = function ( people_db ) {

var people_list = [];

people_db().each(function( person, idx ) { people_list.push( person.name );

});

return people_list.sort().join( ',' );

};

// event handler for 'spa-login' onLogin = function ( event, arg ) { loginEvent = event;

loginData = arg;

$deferLogin.resolve();

};

// event handler for 'spa-listchange' onListchange = function ( event, arg ) { changeEvent = event;

changeData = arg;

$deferChangeList[ changeIdx ].resolve();

changeIdx++;

$deferChangeList[ changeIdx ] = $.Deferred();

};

// Begin /testInitialState/

testInitialState = function ( test ) {

var $t, user, people_db, people_str, test_str;

test.expect( 2 );

// initialize our SPA spa.initModule( null );

Listing B.10 Add our first two tests—webapp/public/nodeunit_suite.js

Declare the first three test methods using descriptive names so that our report reads easily.

Create the makePeopleStr utility. As you might gather from the name, this makes a string containing the names of the people found in a TaffyDB collection. This enables the suite to test the list of online people with a simple string comparison.

Create a method to handle an spa-login custom global event. When this is executed, it invokes $deferLogin .resolve().

Create a method to handle an spa- listchange custom global event.

When this is executed, it invokes

$deferChangeList[idxChange] .resolve(), and then pushes a new jQuery deferred object into

$deferChangeList for subsequent spa-listchange events.

spa.model.setDataMode( 'fake' );

// create a jQuery object $t = $('<div/>');

// subscribe functions to global custom events

$.gevent.subscribe( $t, 'spa-login', onLogin );

$.gevent.subscribe( $t, 'spa-listchange', onListchange );

// test the user in the initial state user = spa.model.people.get_user();

test_str = 'user is anonymous';

test.ok( user.get_is_anon(), test_str );

// test the list of online persons

test_str = 'expected user only contains anonymous';

people_db = spa.model.people.get_db();

people_str = makePeopleStr( people_db );

test.ok( people_str === 'anonymous', test_str );

// proceed to next test without blocking test.done();

};

// End /testInitialState/

// Begin /loginAsFred/

loginAsFred = function ( test ) {

var user, people_db, people_str, test_str;

test.expect( 6 );

// login as 'Fred'

spa.model.people.login( 'Fred' );

test_str = 'log in as Fred';

test.ok( true, test_str );

// test user attributes before login completes user = spa.model.people.get_user();

test_str = 'user is no longer anonymous';

test.ok( ! user.get_is_anon(), test_str );

test_str = 'usr name is "Fred"';

test.ok( user.name === 'Fred', test_str );

test_str = 'user id is undefined as login is incomplete';

test.ok( ! user.id, test_str );

test_str = 'user cid is c0';

test.ok( user.cid === 'c0', test_str );

test_str = 'user list is as expected';

people_db = spa.model.people.get_db();

people_str = makePeopleStr( people_db );

test.ok( people_str === 'Fred,anonymous', test_str );

// proceed to next test when both conditions are met:

// + login is complete (spa-login event)

// + the list of online persons has been updated // (spa-listchange event)

$.when( $deferLogin, $deferChangeList[ 0 ] ) .then( test.done );

Make a jQuery collection, $t, which we can use to subscribe handlers to custom global events.

Subscribe to the jQuery custom global events needed to confirm completion of loginAsFred. The event spa-login is handled by onLogin, and the spa-listchange event is handled by onListchange.

The testInitialState test proceeds without blocking the next test by unconditionally invoking test.done().

Have loginAsFred use jQuery deferred objects to ensure required events have completed before declaring test.done. The sign-in process must be completed ($deferLogin.is_resolved()

===true) and the online person list must have been updated ($deferChangeList[0] .is_resolved===true). The

$.when(<deferredobjects>) .then(<function>) statement implements this logic.

Một phần của tài liệu single page web applications (Trang 400 - 417)

Tải bản đầy đủ (PDF)

(433 trang)