Monday, February 18, 2008

ActiveRecord Double Validation Errors in RSpec

I had a strange error occur in one of my rspec model unit tests today, and I wanted to document it here because my solution (which is a bit of a hack) is the opposite of what worked for other people.

I have a bunch of tests that check to make sure I'm validating various properties of a model. All of a sudden, I started having tests fail because the validations seemed to be adding the same error twice.
'User creation should require domain names to be unique' FAILED
expected 1 error on :domain, got 2
This error only occured when my full suite of tests ran. If I ran the unit test by itself (or even if I just ran only the model tests by themselves), it didn't happen.  Unfortunately I didn't notice whatever it was I did to introduce this error, so I couldn't reverse it.

Several other people have encountered this problem, so I know it's because my tests are leaking state in some way.  Somehow I am doing something during my tests that rspec isn't able to clean up.  Usually it's because the tests are doing something weird with extra require or load statements, which causes multiple copies of the class to be loaded.  Removing these statements usually works.  

I had no such statements and so spent a while trying to sort this out.  On a whim, I added a require statement to the top of the failing User model spec, and that fixed it:
require 'user'
I wish I had more time to investigate, because it makes me thing I don't know enough about Rails autoloading behavior, or Ruby's loading behavior. 

If you're running into this same problem, I found these mailing lists threads to be useful:

Tuesday, February 12, 2008

Unobtrusive Firefox Plugin Click-to-Install

I've been working on a really cool new project, to be announced soon, where I've built a Rails-based web app that has two interfaces: one for humans to interact with inside of a browser, and a RESTful API for browser plugins to interact with via GETs and POSTs.  We want people to be able to interact with our site while visiting other sites.

I had seen other Firefox plugins that were click-to-install, but I had a hard time figuring out how to make it work for our plugin. Firefox users were having to "Save Link As.." and open the downloaded .xpi file manually.  Very old-fashioned.  So here's a quick note to help future Mozilla or Firefox developers who need to create a click-to-install plugin:

1) It's all done through Javascript, so anyone without Javascript will have to install your plugin the old-fashioned way.  The Mozilla site documents the API call you need to make.

2) I'm a huge proponent of unobtrusive javascript (UJS), which I learned by using Dan Webb's excellent LowPro framework. Thus I wanted to make sure that the click-to-install javascript was offered as a progressive enhancement to the normal HTML links we provided.  That way, everyone could have a link to the plugin file itself for manual installation, but people with Javascript could enjoy click-to-install.

In this part of the site, we weren't using any other Javascript libraries, so it seemed like overkill to include Prototype and LowPro just for this one effect.  So it was a great chance to learn how to roll my own UJS without library support.  I whipped up a quick UJS click-to-install technique following inspiration from this presentation.  Here's what I came up with:

<script defer="defer" type="text/javascript">
function doXPITrigger() {
if (!document.getElementsByTagName) return false;
var links = document.getElementsByTagName("a");
for (var i=0; i < links.length; i++) {
if (links[i].className.match("firefox")) {
links[i].onclick = function() {
xpi={'Awesome New Project Toolbar':'/downloads/awesome_project.xpi'};
return false;

window.onload = doXPITrigger

3) I've seen other advice recommending you configure your web server to recognize the .xpi mimetype appropriately.  I did this but it didn't make much difference in my case.  Still, it's probably worth doing.  I added this line to our Apache config:

AddType application/x-xpinstall .xpi