Testing Javascript from Ruby

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.

The example site

What is the bubble sort?

The bubble sort algorithm is a pretty easy algorithm to implement that sorts data. It is not efficient but it has a funny name that describes what happens while the algorithm runs. Greater values move to their final place like bubbles rise in a bottle of water. Therefore an animation is a nice way to explain the algorithm.

The principle of the bubble sort is to compare the first two values of an array, if the first value is greater than the second, it swaps the two values. Afterwards it compares the next two values and does the same. When it reaches the last two values, it starts again until no more values are swapped.

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.

Executing Javascript in Ruby

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.

execute_js.rb
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

The bubble sort implementation is rather easy…

bubble_js/bubble_sort.js
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 automated tests

The tests use the default unit-testing framework of Ruby.

test_bubble_sort.rb
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.

Test sorting an empty array

test_bubble_sort.rb
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.

Test sorting a sorted array

test_bubble_sort.rb
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

Test sorting an array

test_bubble_sort.rb
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

Test sorting an array with duplicate values

test_bubble_sort.rb
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

Test sorting an array of strings

test_bubble_sort.rb
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.