Autotesting Javascript in Rails
I used to love Javascript so much that it would scare me. Why? Because I used to never write any tests. No unit tests. No integration tests (e.g. Selenium). Why? I didn’t know how. Not properly. I didn’t know how to write Javascript unit tests with an autotesting tool, like ZenTest’s autotest command.
And now…
I am now free to write as much Javascript as I like knowing forever I’ll write Javascript tests first, and Javascript in libraries second. TDD for Javascript beckons!
Below is a “getting started” tutorial, a helpful autotesting plugin, and hints about a TextMate bundle for Javascript Unit Testing and a future Peepcode on Javascript Unit Testing.
Installation
Fundamentally, the ideas here are app server-neutral. We’re using the unittest.js library from scriptaculous. But here I’ll show/demo the Rails plugins that make this all uber easy to get started.
Firstly, create a blank rails app, or use an existing app. The tutorial should be non-invasive - just a few “demo” files to delete when you’re done. This tutorial is independent of your app.
ruby script/plugin install javascript_test mkdir test/javascript ln -s ../../vendor/plugins/javascript_test/assets/ test/javascript/assets
For Windows people copy vendor/plugins/javascript_test/assets into test/javascript/assets, as you don’t have symbolic links, which is sad.
At this stage, you can add and commit these files to your project. The rest of this article is destructible demo files and tests.
Creating javascript tests
Let’s say
$ ruby script/generate javascript_test maths
exists test/javascript
exists public/javascripts
create test/javascript/maths_test.html
create public/javascripts/maths.js
maths.js is just a blank javascript file. The important file is maths_test.html. Its packed with lots of “getting started” goodies.
Open test/javascript/maths_test.html and find the testTruth method.
testTruth: function() { with(this) {
assert(true);
}}
This is an example of a javascript test method, with a sample assert call. It has the same api as the Ruby test::unit assert call. Thomas Fuchs’ presentation Adventures in JavaScript testing gives a good rundown of the available assert methods (and the BDD style syntax that is available). I’m also preparing a TextMate bundle to generate the assert calls, with the same “as”, “ase”, “asnn” tab completions as the Ruby bundle. It makes me happy when using it.
Running unit tests
The most holistic way to run your javascript unittests, is against all the browsers you have on your local machine. The javascript_test plugin comes with a rake task for this: test:javascripts
$ rake test:javascripts (in /Users/nicwilliams/Documents/rails_apps/imindi_theme) /test/javascript/maths_test.html on Safari: SUCCESS /test/javascript/maths_test.html of Firefox: SUCCESS Skipping Internet Explorer, not supported on this OS Skipping Konqueror, not supported on this OS
In the browsers you’ll see:
It runs all your tests on all browsers. That’s great for a Continuous Integration process, but for me, coming from the ZenTest world of autotest, I only want to run those tests for which something has changed.
I want continual feedback. Small change, run small test. autotest gives me that for Ruby, but rake test:javascript doesn’t.
Autotesting javascript tests
On my Mac, I’ve set up something as a starting point for autotesting javascript tests, based on the “small change, run small test” principle.
$ ruby script/plugin install http://drnicutilities.rubyforge.org/svn/plugins/javascript_test_autotest/ . . Edit config/javascript_test_autotest.yml for the browser(s) to use for autotesting.
Open config/javascript_test_autotest.yml and uncomment the browser(s) you want to autotest with. I autotest on Safari on the Mac, even though I develop/debug on Firefox, because it reloads each page in the same tab, which I like, and I’m having problems getting this to work into Firefox at all.
My yml file looks like:
browsers: safari: '/Applications/Safari.app/Contents/MacOS/Safari'
So, the current version is known to work for Mac OS X and Safari. If you get this plugin working on different O/S and browsers, let me know below or please submit a patch to http://groups.google.com/group/drnicutilities
From a command line:
$ script/js_autotest Watching public/javascripts/controls.js, public/javascripts/application.js, test/javascript/maths_test.html, public/javascripts/effects.js, public/javascripts/dragdrop.js, public/javascripts/prototype.js Files: 6
Let’s TDD with js_autotest
In test/javascript/maths_test.html, replace testTruth, with the following and save:
// Maths class should exist
testMathsClassShouldExist: function() { with(this) {
assert(Maths, 'Where is Maths?');
}},
js_autotest will automatically load maths_test.html into your browser, and its tests executed.
Now, let’s fix the problem by creating the Maths javascript class. In public/javascripts/maths.js, add:
var Maths = Class.create();
Maths.prototype = {
initialize: function() {
}
}
js_autotest will again automatically reload maths_test.html into your browser, and its tests executed.
Yay for TDD and Yay for “small change, run small test”.
Peepcode
I love Geoffrey’s Peepcodes. Geoffrey has all my money in Peepcode credits. More importantly, Geoffrey’s due to give birth this year to #1 child. Geoffrey, who works from home, is blissfully unaware that soon he’ll be a full-time father and part-time Peepcode author. This could mean fewer peepcodes on important peepcode-worthy topics.
I always wished there was a Peepcode on Javascript Unit Testing. It would take many blog posts to cover as much as can be covered in a single 60 minutes peepcode. So, we’re writing/videoing one.
As a Javascript developer [has anyone ever heard of a Javascript Users Group?] let me know what specifically you’d like covered in this video, so I don’t accidently miss something.
[optional] Install some patches
There was a section here about some patches that could be applied. These have now been applied, and javascript_test plugin has been updated with prototype 1.6.0.1. Life is good.
Find objects in IRB directly from browser URLs
A long time ago, I tired of going into the irb/console and finding objects/models using the traditional ActiveRecord command Person.find(15) and now I’m sitting pretty: I can paste in URLs to fetch objects.
# No more of this: => Person.find(15) # instead: => people/15
people/15 is something you’ll copy+paste directly from your browser: http://localhost:3000/people/15
Of course, the url is based on your routing + controllers, so the assumption here is that your routes/controllers map to your active record models. That is, your app is smothered in RESTful love and cuddles.
Not following this? Here’s a video:
How to make this work at home
Copy and paste the following into your .irbrc file:
Thanks goes to…
The some original code for this comes via Mike Clark, who had the idea for syntax activity(6). This was good.
I previously had another idea to support the syntax 6.to_activity using the RubyGem to_activerecord. I still like the id.to_class_name structure of this and still use it.
But if I have a perfectly nice looking url sitting in front of me, I can now paste the class_name/id part into irb and I’m off and running.
Happy New Year.
RailsRumble hates OpenID
There are 146 RailsRumble entrants.
%w[rubygems hpricot open-uri].each { |l| require l }
(Hpricot(open(”http://vote.railsrumble.com”))/”div.app_summary”).size # => 146
Voting starts Wednesday. I’ve viewed a dozen or so front pages, and they all look awesome. But to use any a lot of them (I think) you need to create an account.
Unfortunately there are only 10 that used OpenID. (see pastie for code)
So for the other 136, you must now create usernames and accounts. Type your email and password 136 times.
UPDATE: RubyBrigade (list of Ruby clubs), Sake Bar (warehouse of Sake recipes), Irksome (irc log), Open Comic Book Database, PubBud, relocatr and Simplebucket do not require any authentication (added below)
Bonus Update: Jour de fĂȘte is a Facebook app. So, in a manner of speaking, it qualifies - Facebook signon is its OpenID equivalent.
For your convenience, here are links to the 10 sites that you can easily log into and play around with, via OpenID:
Soundbadge.Net
A soundbagde is a gravatar like widget, only for sounds instead of graphics. The sounds are individually rendered, based on a personal questionaire.
Hypertr
Linga
The marketing description (a.k.a. nonsense):
Linga is a hypertext meta-universe in which we can all cavort and play. Through the power of teh intertubes, and the the little truck-like links betwixt them, we provide c…
Joglog
You may login as test:test to see real running data.
Admiteer
hey, our band just got \
a gig at a local stage \
let’s sell some tickets \
+++++++++++++
In iambic pentameter:
This weekend we’ve been working ’round the clock \
to make this app functional and complete \
Now as our labor com…
Painboard
Geekhumour.Com
Your ratings are searchable so you can use your highest rated snippets automatically on the site or via rss
Nourish
Fish4brains
Researchr
Ruby Brigade
(todo - Jour de fĂȘte, Sake Bar (warehouse of Sake recipes), Irksome (irc log), Open Comic Book Database, PubBud, relocatr and Simplebucket)
Note: if your site is using OpenID but my script didn’t pick it, let me know below and I’ll add it.
Having suggested that OpenID is the answer to the 146 signups that are required for 146 applications, I actually think I’d be even cooler for RailsRumble (or any other competition) to have a single-signon mechanism, that once you signup, it tells all 146 apps to create you an account and log you in. THAT would be useful.
MyConfPlan for Sale - for Charity
MyConfPlan allows conference attendees to view conference sessions, pick which ones they are attending, see what everyone else is attending, and share what they are attending via personal URLs and cool widgets. Its written in Rails, currently uses Hobo, and a nice slab of Javascript for the cool bits.
And it could all be yours, with an auction on eBay. The auction starts at $1 with NO RESERVE.
But, I’d like to think it could raise $75.
Why? Because all the money paid by the winner is going to the Chad Fowler and Marcel Molina Jr fund for all-things Humanitarian, and so I can go to their Testing Training Day on the 17th of September in Berlin. Minimum donation is $75.
That’s right, the winner’s money could be entirely tax-deductible as its all going to charity. (You get the site’s code base and the myconfplan.com domain (cost of transfer is yours) are yours. I get entry to the workshop.
If it raises $500 then you are donating $500. If it raises $2,000,000 then you are donating $2×10^6.
The site needs an owner/occupier/developer. It needs support leading to upto RailsConf Europe, for example.
At some time in the future, it probably needs to be rewritten. I never wrote (m)any tests. It was built with Hobo, and I built it so fast I crossed the line of “playing with Hobo” and “developing and launching an app with Hobo” very quickly. If you don’t know Hobo, you’ll probably feel more confortable starting some parts from scratch, writing your tests, writing normal Rails controllers and views, etc.
Cool things you can do to MyConfPlan (please add more in the comments if can think of them):
- For the users:
- Let users have “friends” - Show their friends’ on the main session page so they can see immediately which sessions their friends are going to
- Allow Speakers to “own” their session, and allow them to update their session descriptions etc.
- Allow uploading of slides, audio, etc after a session has been completed.
- Give each session a forum before and after the session so people can discuss the session
- Internally:
- Allow re-importing of conference sessions from their source. Conferences keep changing/adding sessions.
- Import hCalendar HTML pages directly. O’Reilly conferences post their sessions in hCalendar microformat, so the data should be directly accessible without the ugly rake task I wrote
- Caching - its got none where it counts
So, get bidding, and own yourself a very cool website.
Sample Rails app: multi-OpenIDs per user
Last time, on “Dr Nic loves OpenID”…
Dr Nic had watched a video by Simon Willison glorifying the delights of OpenID to some Googlers.
Dr Nic decided that each User might like to link multiple OpenIDs to their application account.
Of course, if a user wanted multiple accounts then they could use their different OpenIDs to do that too.
All Dr Nic had to do now, was write some code…
In the mean-whilst…
Later that very same day, Dr Nic reads a tutorial by Joseph Smarr at Plaxo - A Recipe for OpenID-Enabling Your Site. He is joyed that Joseph had said the same thing:
It’s a many-to-one relationship (each user can have multiple OpenIDs attached to their account,
but a given OpenID can only be claimed by a single user)
“Champion!” Exclaims Dr Nic in the seclusion of his underground lair 300m isolated tower kitchen.
This article also lays out an implementation plan. It is in excruciating detail, including table schemas, stylesheet snippets and form suggestions.
Dr Nic rubbed his hands together and gets busy…
[/end of 3rd person]
Sample app
As a developer, the only reason you wouldn’t want to support “multiple OpenIDs per User” is because it is a PITA to implement, in that its an administrative bonus feature for your app. Its not really adding any real uber value.
So I followed his instructions - more or less - an have created a sample app. The README includes a demonstration of the app. You can also download it as a ZIP or TAR bundle.
Now you can add multiple OpenIDs per user and be lazy at the same time.
The app builds on top of Ben Curtis’ OpenID sample app, and uses a variation of Ryan Bates’ Railscasts theme, because its sexy.
NOTE: The code is built for Rails Edge, but the two bundles do not include rails edge, so you need to rake rails:freeze:edge after unpacking it (all this and more in the README). If you want to work with Rails 1.2.3 gems, then.. um, I didn’t write a list of changes, but it might still work, I haven’t tested it. Ben’s original app was built for 1.2.3, so at its very heart it should still work. Perhaps.
Deviations from the Joesph’s tutorial
I want my users to be able to throw their OpenID around on any old page where they see an OpenID field.
That is, allow users to login or register with OpenID from either the “login” form or the “registration/signup” form. Within Rails, these are traditionally two separate controllers (sessions and users respectively).
So, I extracted out this functionality in to a module (helper user_openids_helper) and its used by the two controllers.
How’s it work?
Same way that Ben’s sample app worked, more or less, so read his article a couple times, then read through the source code for the multi-OpenID sample app.
I guess a couple tourist highlights might be:
app/helpers/user_openids_helper.rb- the extract fancy codeconfig/routes.rb- we need more routes to support alternate callbacks from OpenID providerssessions_controller.rbandusers_controller.rb- they are emptier and cleaner nowtest\functionals\sessions_controller.rb- at the bottom are some test cases for the login/registration features ofuser_openids_helper
So, grab your Code Camera…







