Rodrigo Rosenfeld Rosas

Client-side Object Oriented Programming and Testing

Sun, 03 Jun 2012 19:52:00 +0000 (Updated at Tue, 31 Jul 2012 13:30:00 +0000)

Introduction

Despite the fact that I don't like the JavaScript language, we can't just avoid it.

Client-side programming allows for better user experience and less network traffic and is required for lots of web applications. I've been doing client-side code most of my time since 2009 and it takes more and more of my time. I don't think this is gonna change.

Although not perfect, CoffeeScript took a lot of the pain of writing JavaScript code for me, although it still doesn't provide any import/require feature as it has to compile to JavaScript anyway. So, all examples in this article will be written in CoffeeScript, but feel free to write your own tests and code in JavaScript if you prefer.

Since we have a lot of our logic now in the client-side, it is time to take it much more seriously. That means we must write specs (unit and integration ones) for our client-side code as well. That has been a pain for me for a while, but I took some time to release some code to help us with this task, and this is mostly what I'll be talking about in this article. Specially on client-side code integration testing.

Although my released gems depend on Rails Asset Pipeline support, this article should also guide you on how to easily write your specs for whatever server-side framework you've chosen. I'll provide an example on how to do that for a Grails application, but you could apply the instructions for whatever other framework you want.

Design decisions

Feel free to skip this entire section.

Why the Rails Asset Pipeline?

I should state that I'm passionated about Ruby and that Rails is currently my web framework of choice, so be warned that this is probably a biased opinion.

The biggest mistake in the design of the JavaScript language in my opinion was the lack of a require/import statement, which won't allow us to easily split our applications into modules. This was fixed for server-side JS applications by Node.js, but is still an issue for client-side code (that running in web browsers).

ES.Next is going to add modules support for JavaScript but it can take quite a while before 99% of your client users will be using a browser that supports those modules.

Currently I know two alternatives for dealing with dependency management in JavaScript:

  • AMD, with implementations like RequireJS or LabJS, but I find this approach to be too complicated to be practical and I'd rather avoid it;
  • Concatenation by using some pre-processor tool that can process the dependencies.

The Rails Asset Pipeline falls in this second category, just like the Grails Resources plugin. But the Resources plugin will require you to set up your dependencies in a separate file, while in the Rails Asset Pipeline you set up your dependencies as comments in your asset (JavaScript and stylesheets) headers. I much prefer this approach as it reminds me of regular require/import features existing in most programming languages. Also, differently from the Rails Asset Pipeline, the Grails Resources plugin won't support CoffeeScript out-of-the-box.

Also, the Rails Asset Pipeline is well documented and easily extended by the use of plugins (or Ruby gems if you prefer).

My application is not written in Rails!

I'm sorry about you, but this is not a reason for not reading this article. You can still take advantage of the techniques and tools I describe here in whatever framework you're using. Just keep reading on.

Why oojspec?

Please read this article for the reasoning behind it. In short, oojspec is designed with integration tests in mind and an OO approach.

Why object-oriented JavaScript?

I really like OO programming and being able to easily share states. This allows me to write maintainable code and specs in a modular way.

Why CoffeeScript?

I find code written in CS more concise and easier to read. It supports comprehensions, destructuring assignment, splats, string interpolation, array range syntax, "class" and "extend" keywords, "@attribute" as a shortcut to "this.attribute", easy function bindings through "=>", and easier "for-in" and "for-of" constructions among several other great language additions.

On the other side I don't like very much that "==" is translated to "===" and that "elvis?" has a different meaning inside functions and a few other issues I can't remember right now.

But all in all, CS is a much better language than JS in my opinion. Even if you don't want to write CoffeeScript for your production code, you should consider using it at least for your specs. But feel free to use JS for your specs too if you really dislike CS.

So, with CS and the Rails Asset Pipeline which will provide a require-like mechanism, client-side programming is no longer a pain to me. Well, that and the bundled helper tools for helping me out in the testing task, which I'll explore more in-depth in this article.

Why splitting a spec in multiple files?

After writing some specs you can end up with a huge file when writing an integration testing for an application. There will be lots of "describes"/contexts and I'd rather see them split in multiple files for better organization and maintainability. But this is just a suggestion, feel free to use regular "class" constructions in CoffeeScript and put everything in a single file if you prefer.

What about full integration tests?

The integration tests I'll be talking about in this article will use a mocked fake server that will simulate replying to AJAX requests. This will only work for requests using jQuery.ajax (or getJSON/post) which is stubbed by the excelent SinonJS written by my friend Christian Johansen from Gitorious fame.

This will allow the techniques presented in this article to be used with whatever web framework you can think of. Another advantage is that it will run pretty fast by mocking the server-side responses.

Having said that, if you really want to write full integration tests, like with Capybara, this should be pretty easy to achieve if your application is written in Rails. It is just a matter of mounting the spec runner in some route like '/oojspec' for your test environment. Please leave some comment if you want some detailed instructions on how to do that, but be aware that you won't be able to write Ruby code from your JavaScript specs, like filling some initial data in the database through some beforeEach calls... You'd need to add some extra test-only routes for helping you with that.

Enough with the small talk! Get me right into the subject!

Okay, okay, calm down :)

Installing instructions

Non-Rails applications

You'll need a minimal Rails application in some of your application sub-directory.

Here are the instructions for doing so (You'll need Ruby 1.9 and RubyGems installed):

  1. gem install bundler; gem install rake;
  2. Oojs Assets Enabler - just clone it to some first-level subdirectory;
  3. Run "bundle" from this subdirectory;
  4. Optionally symlink the Rakefile to your root directory;
  5. Run "rake oojs:spec_helper" to create a sample spec_helper.js.coffee;
  6. Run "rake oojs:spec -- --name=shopping_cart" to create a sample spec;
  7. Run "rake oojs:serve" to start the server;
  8. navigate to http://localhost:5000 to see your specs passing.

Rails applications

  1. Add the 'oojs' gem to your Gemfile and run "bundle";
  2. rails g coffee:assets shopping_cart; # or js:assets if you prefer
  3. rails g oojs:asset_helper;
  4. rails g oojs:asset shopping_cart;
  5. rake sandbox_assets:serve;
  6. navigate to http://localhost:5000 and see your specs passing.

Organizing your tests/specs

The specs go to "spec/javascripts/*_spec.js(.coffee)". They usually "=require spec_helpers" in the first line.

You're encouraged to split your spec class in several files. Just see the example specs created by the bundled generators.

If you run the spec_helper generator and then run "rails g oojs:asset shopping_cart" (or "rake oojs:spec -- --name=shopping_cart" for non Rails applications), these files will be created:

spec/javascripts/spec_helper.js.coffee:


            
# =require application
# =require modules
# =require jquery
# =require oojspec_helpers
# #require jquery.ba-bbq # uncomment for enabling $.deparam()
#
# Put your common spec code here.
# Then put "# =require spec_helper" in your specs headers.b

You'll need to remove the first "# =require application" line if your application doesn't have an application.js(.coffee) file in the assets path. All other dependencies are provided by the oojs gem.

spec/javascripts/shopping_cart_spec.js.coffee:


            
# =require spec_helper
# =require_tree ./shopping_cart

oojspec.describe 'ShoppingCart', new specs.ShoppingCartSpec

spec/javascripts/shopping_cart/main.js.coffee:


            
extendClass 'specs.ShoppingCartSpec', (spec)->
  initialize: ->
    @createFakeServer()
    @extend this, new specs.oojspec.AjaxHelpers(@fakeServer)

  runSpecs: ->
    @beforeAll -> @fakeServer.start()
    @afterAll -> @fakeServer.stop()
    @before -> @fakeServer.ignoreAllRequests()

    @it 'passes', ->
      @expect(@fakeServer).toBeDefined()

Feel free to add as many files you want inside the spec/javascripts/shopping_cart/ directory.

spec/javascripts/shopping_cart_spec/fake_server.js.coffee:


            
# =require fake_ajax_server

createProducts = -> [
  {id: 1, name: 'One'}
  {id: 2, name: 'Two'}
]

extendClass 'specs.ShoppingCartSpec', ->
  createFakeServer: ->
    @fakeServer = new FakeAjaxServer (url, settings)->
      if settings then settings.url = url else settings = url
      handled = false
      switch settings.dataType
        when 'json' then switch settings.type
          when 'get' then switch settings.url
            when '/products' then handled = true; settings.success createProducts()
#         when 'post' then switch settings.url
#           when ...
#       when undefined then switch settings.type
#         when 'get' then switch settings.url
#           when ...
#         when 'post' then switch settings.url
#           when ...
      return if handled
      console.log arguments
      throw "Unexpected AJAX call: #{settings.url}"

AJAX calls

Whenever your application issue an AJAX request, and that is handled by your fake server, you'll need to decide what to do in your specs. For example, if you click a button and wants to wait for an ajaxRequest to complete, and then process the request, do something like:


            
  @it 'asks for products when clicking on Products button', ->
    $('#products-button').click()
    @waitsForAjaxRequest()
    @runs ->
      @nextRequest '/products', 'get', 'json' # won't pass if such a request wasn't issued
      @expect($('ul#products li:contains(One)')).toExist()

Take a look at ajax_spec_helpers.js.coffee for a list of useful available helpers.

Also take a look at oojspec-jquery.js.coffee for a list of additional matchers for usage with jQuery objects.

Conclusion

There is a lot more to discuss but this article has already taken me a lot of time. I'm intending to write another article creating a test suite for an existent sample application to further demonstrate its capabilities.

Feel free to leave any questions or suggestions in the comments so that we can improve those techniques even more.

Happy client-side coding :)

comments powered byDisqus