before(:all) doesn’t do what you’d expect

Like many before me, last week I was bitten by RSpec’s interesting implementation of before(:all). Although the documentation (http://rspec.info/documentation/) clearly states that before(:all) ‘is run once and only once, before all of the examples and before any before(:each) blocks’, it in fact runs once for every context in the scope. This might be useful, but in every situation I’ve seen it isn’t. The documentation goes on to say

Warning: The use of before(:all) and after(:all) is generally discouraged because it introduces dependencies between the Examples. Still, it might prove useful for very expensive operations if you know what you are doing.

which implies that it really does only get invoked once – you wouldn’t want an expensive operation to be executed for every context.

RSpec’s author has explained that the behaviour is actually as he intended it and proposed to change the documentation, but so far that hasn’t happened.

So, why did I need before(:all) to operate as advertised? I have written a test that collects some metadata and then iterates over it to generate common Examples for every attribute in the metadata. This is great because as the metadata is expanded, the test suite expands automatically ensuring consistent implementation of all of my API. The metadata is nested, so it makes sense to take advantage of RSpec’s contexts to nest the Examples so that the generated test results are self-documenting. Unfortunately, the setup for this test involves one expensive operation: login to the API to get the metadata and prepare to probe every attribute. Logging in takes several seconds. It should be faster, but that’s a different story – my test expands to nearly 5000 examples so even if logging in only took half a second we would still waste over 40 minutes. As it is, with RSpec running my expensive operation for every context, the test was taking over 4 hours!

A blog post helped me on my way, but it focussed on resetting the database between tests and didn’t work with Rails RSpec. I modified it to work with Rails RSpec and refactored it to make it useable in any test. Using it is simple; add the following to your spec_helper.rb file:

# HORRIBLE HACK to work around RSpec's broken before(:all) which does not get run
# once as per the documentation - it runs once at the level defined and once per
# sub-context!
# Extend ActiveSupport::TestCase with a 'before_once' that takes a block just like
# before(:all), but only gets called once at the top-level.
# Inspired by http://sickill.net/blog/2009/11/23/quick-and-dirty-hack-for-rspec-before-all.html
# but recast so that it can be used in any test.
class ActiveSupport::TestCase
  # Like before(:all), but only runs once at the top context and not for every
  # sub-context. It keeps track of the classes that are registered and does not
  # invoke the block in subclasses.
  def self.before_once
    _before_once_class_parents = []
    self.instance_variable_set(:"@_before_once_class_parents", _before_once_class_parents)
    before(:all) do
      unless _before_once_class_parents.select{|g| self.class.to_s =~ /^#{g}/}.any?
        _before_once_class_parents << self.class.to_s
        yield
      end
    end
  end
end

It adds a before_once method to ActiveSupport::TestCase that you can use just like before(:all), but it guarantees that it will only run once per scope.

So now, using before_once, I can ensure that my test only logs into the API once and not once per attribute. The net effect is a test that runs in about 90 seconds rather than taking half a day and climbing!

Ellipsis has an HTML tag…

From the “I never knew that…” department.

There’s an HTML tag for the ellipsis character.  Rather than writing “…” as above if you write &hellip; you’ll get a better-behaved …

It’s in HTML4.0 so it should be everywhere by now!

This is probably worth reviewing for similar goodies.

Posted in HTML. Tags: , . 2 Comments »

A milestone

We’ve just passed revision 10000 in our revision control system (Subversion) which is an opportunity for cake.   Which makes me wonder: what’s the largest revision number out there?

That sort of question implies something about revision control system you’re using …. with the advent of distributed systems such as Git perhaps this sort of question will one day be quaint.

But it’s interesting to note that the subversion project is now officially Apache Subversion so it is managed under the Apache Software Foundation’s Subversion repository and their trunk is currently at revision 982900!

Unexpected logout, expired cookies, sessions and the CKeditor

The Workbooks Desktop, different though it is to most web applications, is typical in that it uses cookies to track sessions. So when you log in a session cookie is stored in the user’s browser and from then on each request from the Desktop has the cookie included in the request.

But, starting a couple of weeks ago we started getting strange behaviour: some users reported getting logged out at various, seemingly random, times. We ruled out all the obvious causes, went through all the changes in our most recent release, but could not see what was going on. It wasn’t specific to any particular user, browser, request, network, operating system or time of day. Or anything else we could think of. We added diagnostic code and all that told us was that after a few hundred perfectly normal Ajax requests suddenly and without any apparent cause there was no cookie included with a request and our Desktop would respond by asking the user to login again. After a few logouts this rapidly becomes unfunny.

Eventually we discovered that we had literally dozens of cookies stored against our domain name in the affected browsers with names like scayt_1__options. A quick search through our source found that our new version of CKeditor had changed so that now the ‘SCAYT’ (Spell Check As You Type) plugin is enabled by default – and that integration creates literally dozens of cookies to hold your spelling preferences!

The browsers didn’t send all these cookies to us every time – they have different Paths to most of our app – but those browsers did discard the much more important Session cookie randomly – because browsers guarantee surprisingly little with regards to the retention of cookies.

Once we’d identified the problem the solution was easy: disable the plugin.