Versão em português
Rodrigo header Rodrigo Rosenfeld Rosas

Client-side code testing with oojspec

Fri, 20 Jul 2012 12:16:00 +0000 (Updated at Tue, 31 Jul 2012 13:00:00 +0000)

Introduction

I've been working solely on single-page web applications for the last 3 years. The client-side code I write is something about 70% of my total code and this percentage has been increasing over the time. While there are excelent tools to work with for testing back-end code in Ruby (RSpec, Capybara, FactoryGirl) I still miss a great framework for writing tests for my client-side code. At least that used to be the case.

We currently have tons of great alternatives for writing client-side code: Knockout.js, Angular.js, Ember.js, Serenade.js and a thousand more. They're awesome for helping us to build single-page applications despite JavaScript being such an horrible language that is only now considering modular programming in ES6, but this will take some years before we can rely on its support :(

Even some languages, like the awesome CoffeeScript, were born to try to make JavaScript code writing more pleasant, although they're still unable to provide something like a require/import statement. After all, they still need to compile to JavaScript :( Fortunately there are some assets pre-processor tools available to help us writing more modular code, like the Rails Asset Pipeline that will allow me to write "require"s as comments in my source headers and that has greatly reduced the pain that is working with JavaScript for me.

But when it comes to integration tests for my client-side code I've never felt great with regards to current available testing frameworks for JavaScript. I've been using Jasmine for a long time but I always missed a beforeAll/afterAll feature. A lot! Mocha/Chai bundle seems great, but unfortunately they require a JavaScript feature that is not present in older Internet Explorer, which I still must support in my products :( Finally, Buster.js is a great modular framework but it is just not suitable for the way I write integration tests because of their random execution order.

Konacha is a great gem that took the right approach on providing some conventions to tests organization being well integrated to the Rails Asset Pipeline. But it used Mocha/Chai... So I created a while ago the rails-sandbox-assets gem with the same goal of Konacha of introducing some conventions to test organization and integrating to the Rails Asset Pipeline. But differently from Konacha, it is framework-agnostic. In fact, I've written adapters for all mentioned testing frameworks in this article:

And recently my own testing framework built on top of Buster.js reporter and assertions:

All those Ruby gems integrate to the Rails Asset Pipeline and all you have to do is creating your tests/specs in specific locations and they will be all automatically loaded by the test runner. Just like it happens with Konacha, this test runner server will only serve the application assets (JavaScript, CSS, images) and won't touch any controllers, models or any other Ruby code.

It is even possible to integrate the Rails Asset Pipeline to non-Rails application, as I've done with this Grails application as a proof-of-concept. See oojs_assets_enabler for a minimal Rails application that can be integrated to any other server framework to enable you to use the power of the assets pre-processor and testing tools with your non-Rails application.

If you don't like the idea of using the Rails Asset Pipeline (because you're averse to Rails or Ruby names), even if it won't require from you any Ruby knowledge, you can still use oojspec standalone. I've created some jsfiddle's examples in oojspec README demonstrating how to do that (or do you think that JsFiddle has included support for Rails as well?! ;) ).

Enough with small talking!

Getting started

Take a look at the reporter first, to see how it looks like.

Yes, I know it is failing. This is on purpose so that you can see the stack-traces and how failures and errors look like.

Setting-up the runner

Rails applications

The oojspec gem will already provide you an HTML runner that will include all your tests/specs located under test/javascripts/oojspec/_test.js[.coffee] and spec/javascripts/oojspec/_spec.js[.coffee] at your taste. Just include the "oojspec" dependency to your Gemfile and run "bundle".

Stylesheets in [test|spec]/stylesheets/oojspec/*_[test|spec].css are also automatically included in the HTML runner. You can just import the required CSS files from them.

Rails Asset Pipeline-enabled applications

If you want to take full advantage of the Rails Asset Pipeline, try to disassociate the "Rails" name from it first. It has nothing to do with Rails at all. You don't have to learn Ruby or Rails for taking advantage of it. Although, if you're using Rails you'll be able to integrate your dynamic routes to your assets. But even if you aren't you can get pre-compilation and minifying tasks, automatic CoffeeScript compiling and, specially, the ability of specifying dependencies between your sources by using special comments in your source headers:

1
2
3
4
5
6
7
// bowling_spec.js
// this will require bowling.js or bowling.js.coffee:
//= require bowling

describe("Bowling", function(){
  // ...
});

Please let me know if you'd like a more in-depth article on how to take full advantage of the Rails Asset Pipeline with your non-Rails application.

All you have to do is to follow the short instructions here. This example has showed how to integrate with Grails but basically all you have to do is to adapt it to add this to your project.

No Rails integration at all

Okay, so you don't see value in the Rails Asset Pipeline or you're using your own tools for pre-processing your assets. Then you'll have to write an HTML runner yourself, which is also pretty simple. Here is a working example in JsFiddle on how to do it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!doctype html>
<html>
<head>
  <base href="http://oojspec.herokuapp.com/" />
  <meta http-equiv="content-type" content="text/html; charset=utf-8">

  <title>oojspec Test Runner</title>

  <link href="/assets/oojspec.css" media="screen" rel="stylesheet" type="text/css" />
  <script src="/assets/oojspec.js" type="text/javascript"></script>
  <script type="text/javascript">oojspec.exposeAll()</script>
<!-- put your code and tests/specs here in the right order of dependency:
  <script src="/assets/first_spec.js" type="text/javascript"></script>
  <script src="/assets/second_spec.js" type="text/javascript"></script>
-->
</head>
<body>

<script type="text/javascript">
  oojspec.autorun()
</script>
</body>
</html>

Feel free to download oojspec.css and oojspec.js for faster local development first.

Describing your code

Now that we have our runner set up, it is time to describe our code by writing some tests/specs.

You can do it with:

1
2
3
4
5
oojspec.describe("Some description", function(){
  this.example("Basic stuff work :P", function(){
    this.assert(true);
  });
});

When using the oojspec gem, by default it will expose the "describe" function to the global (window) namespace, although this can be disabled by adding the following line to your application.rb:

1
config.sandbox_assets.options[:skip_oojspec_expose] = true

Also when using CoffeeScript to write your specs (even if your code is written in JavaScript), that example becomes more succinct. Also, I'm using the exported "describe" this time:


            
describe "Some description", ->
  @example "Basic stuff work :P", -> @assert true

If you prefer to keep with JavaScript, but don't want to type "this." all the time, you can use an alternative idiom:

1
2
3
4
5
oojspec.describe("Some description", function(s){
  s.example("Basic stuff work :P", function(s){
    s.assert(true);
  });
});

From within a description block, the following DSL keywords are available:

Writing your examples

From within an example, you can use any assertion supported by the referee library. All of them are well documented here. You can mix both assertions and expectations in your examples. And you can even write your own custom assertions/expectations.

1
2
3
4
5
6
7
8
oojspec.assertions.add("isVisible", {
  assert: function(actual) {
    return $(actual).is(':visible');
  },
  assertMessage: "Expected ${0} to be visible.",
  refuteMessage: "Expected ${0} not to be visible.",
  expectation: "toBeVisible"
});

Asynchronous examples

Sometimes you need to wait for certain conditions after taking some actions and those will most probably happen in an async fashion. So, for letting you focus in the specs instead of having to write polling functions yourself, oojspec borrows the waitsFor/runs approach from Jasmine.

1
2
3
4
5
6
7
8
9
10
11
describe("Some description", function(s){
  s.example("Operation was successful", function(s){
    $('button#create').click();
    s.waitsFor("dialog to pop up", function(){
      return $('#show-message-dialog:visible').length > 0;
    });
    s.runs(function(s){
      s.expect('#show-message-dialog').toBeVisible();
    })
  });
});

You can use multiple waitsFor and runs blocks in the same example at your will.

Mocks

Sometimes mocks are really useful. Specially for creating fake HTTP servers for responding to your application AJAX requests. But since they're orthogonal to test runners, no mocking library is included in oojspec. But I'd recommend you using the excellent Sinon.js mocking and stubing library. If you're using the Rails Asset Pipeline, this is just a matter of including the sinon-rails gem to your Gemfile and requiring it in your spec:

1
//= require sinon

Sinon.js has a fake AJAX server built-in but if you always use jQuery for your AJAX requests you might find my gem fake-ajax-server somewhat easier to use.

Object-oriented testing

Specially when writing integration tests for my client-side code, I find it easier to describe a group of behaviors like sequential examples that are depending on a given order. In those cases I find it useful to share some state between them and taking an object-oriented approach would take care of this.

Suppose you have some class that you instantiate on your application load that will take care of registering some jQuery live events which are never unregistered because it is not needed by your application. So, you're unable to instantiate such a class several times in "before" hooks because you'd be registering the same events several times. In that case, you can instantiate it in a "beforeAll" hook once in your suite.

But then it will be impossible to get back to the original state. But I don't see this as a major issue. Suppose you have to test a dynamic tree, using the excellent jqTree library. You can start with an empty tree and add a test for including a new item to the tree. Then you add another test for including a sub item to the item created in your prior test. Then you add a test for moving it so that it becomes a sibling of the first item. Then you add a test for deleting the first item and make sure only the last one is kept. I don't really mind if all those tests written for a "Tree Management" context are not independent from each other. I find it easier to write those tests in this sequential order than trying to make them independent.

This is the main point where I find the other testing frameworks to be too limiting for me or they don't target the same browsers as I do.

When writing non-oo tests with oojspec, "this" will refer to an object containing only the available DSL for that context. This same DSL object is also sent as the first arguments to the blocks used by example, context, runs, etc.

On the other hand, when writing OO tests, you are in charge of specifying what will "this" refer to.

By default, OO tests are "non-bare", which means that the DSL will be merged with your "this" object. This allows you to write "this.example" as before. But you can opt for using a "bare" approach in which case you'll handle the DSL through the first argument of the block.

You can provide the description directly in the passed object or as the first argument as before. It is only required that your object responds to runSpecs() as the entry point.

Here are some examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// non-bare approach, with the description in the object itself:
describe({
      description: 'Plain Object binding',
      dialog: {dialog: true},
      runSpecs: function(){ this.example('an example', this.sampleExample); },
      sampleExample: function(){ this.assert(this.dialog.dialog); }
});

// traditional description syntax and a bare approach:
describe('Bare description', {
      bare: true,
      dialog: {dialog: true},
      runSpecs: function(s){ s.example('an example', this.sampleExample); },
      sampleExample: function(s){ s.assert(this.dialog.dialog); }
});

In case you prefer CoffeeScript, like me, you can find the "class" syntax somewhat easier to work with. oojspec will instantiate a class in case it detects it is a class (its prototype responds to runSpecs instead of the object itself). It even uses the constructor's name if a description is not provided.


            
describe class # you can use an anonymous class as well
  @description: 'Bare class'
  @bare: true

  runSpecs: (dsl)->
    @dialog = dialog: true
    dsl.example 'an example', @anExample
    dsl.context 'in some context', @aContext
    dsl.describe NonBareClass

  anExample: (s)-> s.expect(@dialog).toEqual dialog: true

  # this.runs is not available from an example when using a bare approach
  aContext: (s)-> s.example 'another example', (s)-> s.refute @runs

class NonBareClass # description will be "NonBareClass"
  runSpecs: ->
    @dialog = dialog: true
    @example 'an example', @anExample
    @context 'in some context', @aContext

  anExample: -> @expect(@dialog).toEqual dialog: true

  # this.describe is never available from within an example
  aContext: -> @example 'another example', -> @refute @describe

Real examples

This article is already long enough. I'll try to find out some time in the future to focus in some real use case to demonstrate how I write integration tests for my single-page applications using some real application as an example.

Feedback

I'd really love to hear your feedback about oojspec. Please let me know what you think about it by e-mail, GitHub, comments in this page or Twitter (rrrosenfeld). If you think you've found some bug, please report it on GitHub issues.

comments powered byDisqus