Developing a site using one of the numerous Ruby Web frameworks does not help in automating the tests for the client-side code (Javascript code). How is it possible to test that code? How to automate such tests?
Testing user interactivity is pretty hard to automate not to say that it coulld be a waste of time…
If the Javascript code contains complex algorithms that can be refactored so that they do not need to play with buttons, input fields or to change the DOM, next approach can be used: automate the Javascript tests from Ruby.
As an example to show the technique, imagine a programming tutorial presenting the bubble sort algorihm. The tutorial includes a sorting animation as shown hereafter, clicking the start button plays an animation and sorts the numbers1.
Apart from the animation implementation, the implementation of the sorting algorithm is tested on the server-side.
The server-side code is written in Ruby and integrating tests for another language that runs inside the browser could be a hard work.
Some Ruby gems bring Javascript support to Ruby. The ExecJS gem offers a common runtime interface relying on an actual Javascript runtime. Unfortunately, the behaviors of the underlying runtimes are not identical, the code presented here was (re-)written so that it runs with the Node.js/V8 and the therubyracer/V8 runtimes.
Once the gem and a runtime are installed, a Ruby script can execute Javascript code.
1 2 3 4 5 6 7 8 9 |
require "execjs" puts "Javascript runtime: #{ExecJS.runtime.name}" js_code = "var x = 'hello';" ctx = ExecJS.compile(js_code) puts "x => #{ctx.eval('x')}" |
Line 3 prints out the underlying Javascript runtime.
Line 5 assigns a very short Javascript snippet to the js_code
variable that is compiled on line 7. The compile
name is a bit overused because compile
actually runs the code. Anyway, the pseudo compilation produces a context.
Line 9 retrieves the value of the x
variable to which the Javascript code assigned the "hello"
value.
Here is the result.
$> $HOME/.rvm/wrappers/ruby-2.2.0@execjs/ruby execute_js.rb Javascript runtime: therubyracer (V8) x => hello
The bubble sort implementation is rather easy…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/* * Sorts the given array (data) in place using the bubble sort * algorithm. * * Calls the given monitor function (swapMonitor) for each swap if * given. * * Returns the array. */ function bubbleSort(data, swapMonitor) { var n = data.length - 1; for (;;) { var i = 0; var didSwap = false; while (i <= n) { if (data[i] > data[i + 1]) { var x = data[i]; data[i] = data[i + 1]; data[i + 1] = x; if (swapMonitor) swapMonitor(i, i + 1); didSwap = true; } i++; } if (!didSwap) { break; } } return data; } |
The code has two loops: the first (line 14) restarts the second loop (line 20) until no more swap happens (breaking the loop on line 39). The second loop iterates through all the values, swaps the values (lines 24-26) and keeps track of the swap (line 30).
The bubbleSort
method has two parameters, the first is the array to sort and the second, that is optional, is a callback method. The callback happens for every swap needed for the animation (line 28).
A simple improvement of the algorithm is to keep track of the lower positions that were not swapped so that the iteration does not start from 0 on every loop, not applied here.
The implementation does not depend on any browser object and does not require any action from the user.
The tests use the default unit-testing framework of Ruby.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
require "execjs" require 'test/unit' class BubbleSortTest < Test::Unit::TestCase def setup js_file = File.join(File.dirname(__FILE__), 'bubble_js/bubble_sort.js') code = File.read(js_file) @context = ExecJS.compile(code) end end |
The test framework calls the setup
method before each test that loads (lines 8-10) and compiles (line 12) the Javascript under test. It stores the produced context in an instance variable named @context
.
1 2 3 4 5 6 7 |
def test_sort_an_empty_array actual = @context.eval('bubbleSort([])') assert_equal [], actual end |
$> $HOME/.rvm/wrappers/ruby-2.2.0@execjs/ruby test_bubble_sort.rb Loaded suite /tmp/release/test_javascript_from_ruby/test_bubble_sort Started . Finished in 0.008368926 seconds. 1 tests, 1 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 119.49 tests/s, 119.49 assertions/s
The test executes the call to the bubbleSort
method on line 3 and checks the result on line 5.
1 2 3 4 5 6 7 |
def test_sort_a_sorted_array actual = @context.eval('bubbleSort([3,13,17,18,19,53,111])') assert_equal [3,13,17,18,19,53,111], actual end |
$> $HOME/.rvm/wrappers/ruby-2.2.0@execjs/ruby test_bubble_sort.rb Loaded suite /tmp/release/test_javascript_from_ruby/test_bubble_sort Started .. Finished in 0.836193067 seconds. 2 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 2.39 tests/s, 2.39 assertions/s
1 2 3 4 5 6 7 |
def test_sort_an_array actual = @context.eval('bubbleSort([17,1,-8,80,21,11,13])') assert_equal [-8,1,11,13,17,21,80], actual end |
$> $HOME/.rvm/wrappers/ruby-2.2.0@execjs/ruby test_bubble_sort.rb Loaded suite /tmp/release/test_javascript_from_ruby/test_bubble_sort Started ... Finished in 0.009638343 seconds. 3 tests, 3 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 311.26 tests/s, 311.26 assertions/s
1 2 3 4 5 6 7 |
def test_sort_with_duplicates actual = @context.eval('bubbleSort([13,1,13,80,21,11,13,77])') assert_equal [1,11,13,13,13,21,77,80], actual end |
$> $HOME/.rvm/wrappers/ruby-2.2.0@execjs/ruby test_bubble_sort.rb Loaded suite /tmp/release/test_javascript_from_ruby/test_bubble_sort Started .... Finished in 0.009132984 seconds. 4 tests, 4 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 437.97 tests/s, 437.97 assertions/s
1 2 3 4 5 6 7 |
def test_sort_an_array_of_strings actual = @context.eval("bubbleSort(['z','a','b','hello','g','world'])") assert_equal ['a','b','g','hello','world', 'z'], actual end |
$> $HOME/.rvm/wrappers/ruby-2.2.0@execjs/ruby test_bubble_sort.rb Loaded suite /tmp/release/test_javascript_from_ruby/test_bubble_sort Started ..... Finished in 0.00999942 seconds. 5 tests, 5 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 500.03 tests/s, 500.03 assertions/s
Next part implements more tests and talks about benchmarks.