Or how to use Testem in Rails project without touching a single line of Ruby code.

Transition to the rich client

It is uncommon nowadays to see Rails application getting split into the rich client app and Rails serving API. I recently found myself spending more and more time writing JavaScript code and I am sort of missing flow I used to have with Rails, the reason why - lack of same TDD easiness.

Ruby testing frameworks are not sufficient and/or natural

It is not like JavaScript code cannot be tested with existing Ruby testing frameworks, it can, people were using Capybara with drivers supporting JavaScript for years, mainly for integration testing. However:

  • A lot of logic and complexity moves to JavaScript which means code has to be tested much deeply. Components have to be tested in isolation from each other, third party APIs have to be stubbed, etc.

  • Needless to say, full Rails stack tests are much slower than pure JavaScript tests. The integration test suite is the heaviest one amongst Rails test suites.

  • Testing against real browser with Selenium driver launches Rails server in separate thread, which sometimes causes synchronization issues, your test might become flipping randomly.

  • If you have a JavaScript programmer in your team who is not very familiar with Ruby, Rails, Bundler, Capybara etc., she could have troubles writing and maintaining tests for JavaScript code unless she really want to dive deeper in Ruby ecosystem.

Test client code with client tools

So what the solution? Yes, to test client code with client tools. I recently have been using Testem/Jasmine and found it going well with my test habits. What do we have out of the box:

  • RSpec style JavaScript tests with Jasmine which is part of Testem.
  • Multiple browsers running test suites.
  • Headless test running if PhantomJS is installed
  • Continuous testing, i.e. Testem watches test files and their dependencies and re-run test on every change.

How to generate server responses?

When your Rails app is running it is sort of clear how to write tests, just use http://localhost:3000. What is not clear - what to do if Rails app is not running: something must emulate its responses. What that something could be? Since in core, Testem is a Node.js web server, responses could be generated by that server. That approach is what Lineman takes. Other approach would be to stub requests on the client using some mocking facilities, for example Jasmine spies. I was looking for something very simple, as simple as grabbing server response and putting it into the file. After all Testem already does serve test pages, may be it could serve fixtures? Fortunately the answer is “yes” and this is pretty easy to do.

Contrived Rails app

We load list of states via Ajax and populate a popup with that list. Lets create Rails project with controller and action:

    > rails new testem_demo
    > cd testem_demo
    > rails g controller states index
     #./config/routes.rb:
     #get "states/index"
     resources :states, :only => [:index]

     #./app/controllers/states_controller.rb
     class StatesController < ApplicationController
       def index
         respond_to do |format|
           format.json { render :json => ['CA','IL','AL'] }
           format.html
         end
        end
     end

Launch Rails server and direct your browser to http://localhost:3000/states.json, you should see somewhat like:

["CA","IL","AL"]

First failing test

Lets decide where our test files are going to reside. Usually Rails test are located in Rails.root/spec or in Rails.root/test. However there are couple reasons not to put our JavaScript tests there:

  • Emphasize the fact the our JavaScript code and Ruby code are isolated.
  • Keep tests close to the code their cover

However it is probably matter for taste. So lets create file {Rails.Root}/js_tests/test_index.js with simplest Jasmine test:

    // ./js_tests/test_index.js
    describe("states", function() {
      it ("list states", function() {
      });
    });

How to run it? We need to install Testem first, please follow installation instructions. When you done you should be able to launch testem from the command line. It is good idea to install PhantomJS and have phantom executable in your system. When testem detects phantom executable it automatically picks it for headless test environment.

Lets see if your test succeeds:

    >cd ./js_tests
    >testem

It does! You will see somewhat like: testem

The test does not verify anything so lets make it failing. You may want to keep Terminal window with running testem open, so you would be able watch test failing. Lets implement the test:

    // ./js_tests/test_index.js
    describe("states", function() {
      it ("list states", function() {
          spyOn($, "ajax");
          App.listStates();
          expect($.ajax.mostRecentCall).toBeDefined();
          expect($.ajax.mostRecentCall.args[0].url).toEqual("/states");
      });
    });

What we are doing here - we are mocking $.ajax call and then calling our (not yet created) function App.listStates() and in last two lines we are checking if Ajax request has been made and to what URL. The $.ajax mock is what “catches” this information. Good, we can see the failure:

    ReferenceError: Cant find variable: $ in
    http://localhost:7357/test_index.js (line 3)

Sharing assets with Rails

Our test expects $.ajax() is getting called and it fails because variable $ cannot be found. We need to have jQuery loaded in order to fix this. Normally, it is being provided by Rails asset pipeline, but Testem knows nothing about it. However it is possible to specify what JavaScript files have to be loaded in Testem config file. Make sure you have jQuery.js in ./vendor/assets/javascripts/, then create a Testem config file ./js_tests/testem.json:

    // ./js_tests/testem.json
    {
      "src_files": [
        "../vendor/assets/javascripts/jquery.js",
        "test_*.js"
      ]
    }

Still same error:

    ReferenceError: Cant find variable: $

For some reason, Testem does not understand relative paths. We could fix it by copying jQuery.js into js_tests directory, but that would be a bad idea - we are going to have two versions of jQuery.js library, so we will need to keep them in sync. Fortunately there is another way:

    // ./js_tests/testem.json
    {
      "src_files": [
        "vendor/jquery.js",
        "test_*.js"
      ],
      "before_tests": "ln -fhs ../vendor/assets/javascripts vendor"
    }

before_tests options is command line Testem executes before running tests and our command creates a symbolic link vendor to the directory where jQuery.js is located. I ran it on Mac OS X, on Linux command would be different, but it is trivially could be wrapped to platform agnostic shell script.

Time to implement our JavaScript code!

Now the error Testem complains about is different:

   ReferenceError: Cant find variable: App

Which is indication we have to implement some application code: Lets place it in ./app/assets/javascripts/app.js:

    // ./app/assets/javascripts/app.js
    var App = App || {};

Do not forget to add app.js to Testem config (with the same link trick we used to add jQuery.js):

    // ./js_tests/testem.json
    {
      "src_files": [
        "vendor/jquery.js",
        "app/app.js",
        "test_*.js"
      ],
      "before_tests": "ln -fhs ../vendor/assets/javascripts vendor; ln -fhs ../app/assets/javascripts app"
    }

and to the assets pipeline:

    #./app/assets/javascripts/application.js
    //...Generated by Rails
    //= require app

The error is different now:

    TypeError: 'undefined' is not a function (evaluating 'App.listStates()')

Which means we are making a progress. Lets implement App.listStates():

    // ./app/assets/javascripts/app.js
    var App = App || {};
    App.listStates = function() {
       $.getJSON( '/states' );
    }

Cool, green test!

Making fixtures

But we are not done yet. We only making an Ajax request, we also need to populate popup with list of returned states. Test goes first:

    // ./js_tests/test_index.js
    describe("states", function() {
      it ("populate popup with states", function() {
          App.listStates();
          expect($('#popup option').length).toEqual(3);
      });
    });

It fails, as expected:

    Expected 0 to equal 3.

Lets implement App.listStates():

    // ./app/assets/javascripts/app.js
    var App = App || {};
    App.states = null;
    App.listStates = function() {
      $.getJSON( '/states' ).done( function(data) {
        App.states = data;
        var $popup = $("<select id='popup'></select>");
        App.states.forEach( function(state) {
          $popup.append('<option>' + state + '</value>');
        });
        $('body').append($popup);
      });
    }

Same error! What is going on? It is time to open the browser and inspect. Just point your browser to http://localhost:7357/:

testem

In Chrome Developer Tools/Network pane you should see:

GET http://localhost:7357/states 404 (Not Found)

It explains failure, popup cannot be populated because Ajax call does not return anything, the resource /states is not available. We are going to create a fixture for it. Fortunately it is very easy:

  • launch your Rails server, go to http://localhost:3000/states.json

  • copy result into file ./js_tests/states:

      // ./js_tests/states
      ["CA","IL","AL"]
  • you may stop Rails server.

After grabbing a response and placing it to the fixture file, we do not need to keep Rails server running. From now Testem will process GET /states request and reply with fixture file. Go back to Chrome Development/Network and check it! Testem acts as a file server and we use it to send us our fixture data.

Code flow != control flow

However test still fails:

Expected 0 to equal 3.

Lets examine - do we really have #popup element populated? console.log is our friend here:

    // ./app/assets/javascripts/app.js
    var App = App || {};
    App.states = null;
    App.listStates = function() {
      $.getJSON( '/states' ).done( function(data) {
        App.states = data;
        var $popup = $("<select id='popup'></select>");
        App.states.forEach( function(state) {
          $popup.append('<option>' + state + '</value>');
        });
        $('body').append($popup);
        console.log($("#popup option").length);
      });
    }

You will see right number 3 is getting printed. May be our test is incorrect? Here is the test code:

    // ./js_tests/test_index.js
    describe("states", function() {
      it ("populate popup with states", function() {
          App.listStates();
          expect($('#popup option').length).toEqual(3);
      });
    });

Assertion follows immediately after making an Ajax call. The Popup gets populated later in $.getJSON callback. We asserting too early, what we want is to wait for server response, allow App code to process it and only then verify the state. It happens Jasmine have a facilities just for that, they are waitsFor and runs methods.

  • waitsFor’s “holds” a flow until condition becomes true.
  • runs ensures the code in its callback argument is executed after waitsFor.

Lets apply them to our test:

    describe("states", function() {
      it ("populate popup with states", function() {
        App.listStates();

        waitsFor( function(){
          return App.states !== null;
          }, 200, 'GET /states fails');

        runs( function(){
          expect($('#popup option').length).toEqual(3);
          });
      });

    });

Bottom lines

Hooray! We built our app with TDD, and on the way we solved couple issues:

  • Testem shares same JavaScript assets with our application
  • Fixtures could be made by copying and pasting server responses to the files.

Note: Fixtures are not greatest testing practice and Rails developers know they could make tests fragile. But thats the price of having not 100% integrated tests. After all Rails developers use VCR for capturing third parties APIs and re-playing it. This approach is sort of similar - the Rails app is being treated as third party service. Also it is trivial to write rake task generating those fixtures.