Welcome to Proudly ProcrasDonating :-)
Subscribe to my RSS feed and
visit ProcrasDonate.com
I’d like to share with you a neat function we wrote to sequentialize testing tasks.
Most of our tests are not concerned with timing, but we do have a few dozen time tracking scenarios. Each scenario re-enacts a user taking particular actions for particular durations. In addition to viewing webpages, we throw in application switches, the computer going to sleep, and other time tracking influences.
We’re particularly interested in how different time tracking triggers interact with each other, especially since some of the notifications can be delayed up to 5 seconds.
An important requirement is that we be able to write a test that plainly lists the actions and durations to take; such as the following:
- Start viewing a web page; say, http://foo.com/bar
- 30 seconds later notify the “user is idle” observer
- 40 seconds later notify the “user is back” observer”
- 10 seconds later start viewing a new web page
We expect to nicely abstract the test framework stuff that checks the actual behavior against the expected behavior (in this case a 30 seconds visit followed by a 10 second visit to web page http://foo.com/bar).
Algorithm Overview
The Javascript tool at our disposal is
, which waits for a given duration before executing a given expression.
Thus, the core of our sequentializer is turning a list of expressions and durations into a setTimeout expression that itself calls setTimeout with an expression that calls setTimeout… The recursive nature of the algorithm is best viewed through pseudocode:
# input items: list of (action, duration) pairs # [ {action: some function, duration: seconds}, # ..., # {action: some function} # ] # goal is to execute items[idx].action, wait items[idx].duration, # and then execute items[idx+1].action, wait items[idx+1].duration, # and then execute items[idx+2].action, .... until all actions # are executed. # The duration field, if present, in the last dictionary in items # is ignored. # # input idx: index into items of next pair to execute sequentialize( items, idx ) # if there is an action to execute, execute it if idx < items.length execute( items[idx].action ) fi # if there are more actions to execute, recurse if idx + 1 < items.length setTimeout( sequentialize(items, idx+1), items[idx].duration ) fi
Javascript Solution
When we converted this pseudocode to a Javascript sequentializer function, we added a _create_sequentializer helper method to do testing stuff so that each test case need only specify the items data structure.
Here’s the full Javascript:
/** * @param items: list of functions to execute. * has the following form: * [ * {fn, self, args, interval}, * {fn, self, args, interval}, * ... * ] */ sequentialize: function(items, idx) { var self = this; if (idx < items.length) { items[idx].fn.apply(items[idx].self, items[idx].args); if (items[idx].interval) { setTimeout(function() { self.sequentialize(items, idx+1); }, items[idx].interval); } else if (idx+1 < items.length) { // flexible: in case we have actions we don't want to wait on self.sequentialize(items, idx+1); } } }, /** * Test framework stuff */ _create_sequence: function(testrunner, display_results_callback, site, actions) { var self = this; var expected_durations = []; var sequence = []; _iterate(actions, function(key, action, index) { if (action.expected_visit) { expected_durations.push(Math.round(action.interval/1000.0)); } sequence.push({ fn: action.fn, self: action.self, args: action.args, interval: action.interval }); }); // append tests to sequence sequence.push({ fn: self.check_visits, self: self, args: [testrunner, display_results_callback, site, expected_durations], interval: 1 }) // initiate sequence execution self.sequentialize(sequence, 0); }, // here is the above example test case in code // it reenacts a user initiating a view of a webpage, going idle, // coming back, and then ending that page view self._create_sequence( testrunner, display_results_callback, site, [{ fn: self.time_tracker.start_recording, self: self.time_tracker, args: [site.url], interval: Math.floor(Math.random()*7000)+2000, expected_visit: true }, { fn: self.idle_back_listener.idle, self: self.idle_back_listener, args: [], interval: Math.floor(Math.random()*7000)+2000 }, { fn: self.idle_back_listener.back, self: self.idle_back_listener, args: [], interval: Math.floor(Math.random()*7000)+2000, expected_visit: true }, { fn: self.time_tracker.stop_recording, self: self.time_tracker, args: [], interval: 0 }] );
Proudly ProcrasDonating,
Lucy.








