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:

Labels: , ,

Wednesday, January 23, 2008

Shout out to Rails Envy & More Autotest Love

Thanks to the Rails Envy Podcast for mentioning my note on autotest configuration. Another aspect I enjoy about using the verbose flag is that autotest tells you exactly what has triggered the re-test. Kind of a fun way to look under the hood of your testing system:
[["app/controllers/admin/users_controller.rb", Wed Jan 23 14:06:10 -0600 2008]]
/usr/local/bin/ruby -S script/spec -O spec/spec.opts spec/controllers/other_inboxes_controller_spec.rb spec/controllers/admin/users_controller_spec.rb
If you like that, you might also be interested in the autotest timestamp plugin, which stamps autotest runs like so:
# Waiting at 2008-01-23 14:06:17
All you have to do is uncomment this line in your ~/.autotest file:
require 'autotest/timestamp'

Labels: ,

Wednesday, January 16, 2008

Autotest with verbose flag on

I followed a tip from David Chelimsky's blog and began running autotest with the -v flag for verbosity.  When you first run it, you get a bunch of lines like this:
Dunno! spec/other_inbox_spec_helpers.rb
Dunno! app/views/layouts/main/_footer.erb
Which show all the files for which autotest doesn't have a mapping.  With ZenTest 3.8.0 out, it's easy to add mappings (which tell autotest which tests to run when a matching file changes) and exceptions (which tell autotest which files to ignore).  You can also set up .autotest files for particular projects, or for your whole development machine (~/.autotest).

Here's an example of a per-project .autotest I use, sitting in the Rails root directory.  I've got a custom spec helper and I want to rerun all my tests if this file ever changes:
Autotest.add_hook :initialize do |at|
%w{ domain_regexp perfdata coverage reports }.each { |exception| at.add_exception(exception) }

at.add_mapping(/spec\/app_spec_helper.rb/) do |_, m|
at.files_matching %r%^spec/(controllers|helpers|lib|models|views)/.*\.rb$%
end

end
And here's part of my site-wide .autotest file, mostly cribbed from David's blog, where I'm ignoring other kinds of cruft that pile up in projects.  Also note the mapping for spec/defaults.rb, a file I commonly setup in my specs containing default parameters for different models.
Autotest.add_hook :initialize do |at|
%w{.hg .git .svn stories tmtags Rakefile Capfile README spec/spec.opts spec/rcov.opts vendor/gems autotest svn-commit .DS_Store }.each {|exception|at.add_exception(exception)}

at.add_mapping(/spec\/defaults.rb/) do |f, _|
at.files_matching %r%^spec/(controllers|helpers|lib|models|views)/.*\.rb$%
end

end

Labels: , , ,

Sunday, August 26, 2007

Cache Test Dummy

I use page caching in a lot of my projects, but could never figure out how to test them properly. There's a pretty good plugin that does this but I could never get it to work right. Until today, when I noticed that caching is turned off in testing mode! No wonder I couldn't test it!

So, it seems obvious now, but it never occurred to me to change this line in my test.rb file:

config.action_controller.perform_caching = true


Note that you will have to make sure your pages get swept properly after running your tests. I have a rake task that deletes all of the files, I think I got it from the Peepcode caching episode:


desc "Delete all cached files"
task :sweep_cache => "tmp:cache:clear" do

%w(index.html shows* about.html audio.html contact.html photos.html photos/*.html).each do |pattern|

rm_rf(Dir[File.join(RAILS_ROOT, "public", pattern)])

end

end



A little clunky, but it works. I might someday come up with a way to invoke all of my sweepers instead so I don't have to manually add all of those filenames. I think call this task from an autotest hook:




[:red, :green].each do |hook|
Autotest.add_hook hook do |at|
`rake sweep_cache` if File.exist?("#{FileUtils.pwd}/lib/tasks/cache.rake")
end
end



Because the plugin doesn't have many examples, here's a couple of my tests:
caching_story_test.rb



class CachingStoryTest < ActionController::IntegrationTest
fixtures :all

def test_page_caching
assert_cache_pages(about_path, contact_path)
end

def test_storyteller_caching
storyteller = storytellers(:storyteller_show_1_id_1)
assert_cache_pages(show_storyteller_path(:id => storyteller.id, :show_id => storyteller.show.id))
end

end



sweeping_story_test.rb


class SweepingStoryTest < ActionController::IntegrationTest

fixtures :all
include LoginLogoutDSL
include StoopDefaults

def test_should_sweep_news_items

logs_in
news_item = news_items(:double_whammy)

assert_expire_pages(news_items_path, news_item_path(news_item), index_path) do |*urls|
put news_item_path(:id => news_item.id), :news_item => @@news_item_default_values
end

assert_expire_pages(news_items_path, index_path) do |*urls|
post news_items_path, :news_item => @@news_item_default_values
end

assert_expire_pages(news_items_path, news_item_path(news_item), index_path) do |*urls|
delete news_item_path(:id => news_item.id)
end

end
end


(I'm sure there's a way to DRY up that last test, but I found it much harder to figure out which test was failing when I did that.

Last note: the expiration test only work with path routes, not url routes. So you call "delete news_item_path(...)" vs "delete news_item_url(...)".

Labels: ,

Wednesday, May 30, 2007

Functional Testing for Attachment-fu

I found a lot of good tutorials on how to setup attachment_fu (which lets you easily store images or other binary data in a Rails app, either in the filesystem or in the database itself), but none of them explain how to write proper functional tests. Since a lot of other people have been posting this question, I thought I would post what I just got working:

def test_should_create_new_attachment

fdata = fixture_file_upload('/files/photo1.jpg', 'image/jpeg')

login_as :bob

assert_difference Photo, :count, 2 do
post :create, :photo => { :uploaded_data => fdata }, :html => { :multipart => true }
end

assert_redirected_to user_url(users(:bob))
assert_valid assigns(:photo)

end

The above assumes you create a 'files' directory inside of your fixtures directory. Also note that if you have thumbnailing enabled, each file you upload will create multiple attachment objects (in this case, one to represent the original image and one to represent a thumbnail).

If you're curious, here's how I have the plugin configured:

class Photo < ActiveRecord::Base

has_attachment :content_type => :image,
:storage => :file_system,
:max_size => 500.kilobytes,
:resize_to => '320x200>',
:thumbnails => { :thumb => '100x100>' },
:processor => 'ImageScience'

validates_as_attachment


end

Labels: ,

Sunday, May 20, 2007

Rcov is awesome but not without these tweaks

I've recently started using the rails_rcov plugin to test how well I am testing the code I am writing. It's been a great help, but I was getting very strange results until I read this post about how to configure rcov properly to concentrate its testing. (Basically you have to force it to run your unit and functional tests in isolation which seems counterintuitive). It's easy enough to add parameters to your rakefile that will straighten it out.

Now I am going to try to get Heckle working for even better testing of my tests.

Labels: ,

Sunday, April 22, 2007

Taming assert_select with css_select and Regexp.escape

I had a lot of trouble writing a test to check whether all elements of an array were showing up in a table properly; I needed more fine-grained testing than assert_select was allowing me. I needed to guarantee that every element in an array could be found as a child of a particular table.

Fortunately, I noticed that Rails also has a css_select method which returns an array of selected elements without running tests on them. This lets you run your own tests using the results of the selection.

This isn't the prettiest code in the world (I'm also testing a formatting helper, hence the literals '$200.00' and so on), so I'll probably refactor it later. There might also be a way to do this more elegantly with assert_select. But I think this does show the utility of css_select.
elements = css_select('table#investors tr td')

[[investors(:investor_8),'$200.00'],
[investors(:investor_9),'$343.40']].each do |i,amount|

assert_not_nil elements.detect \
{ |e| e.to_s =~ /#{i.name}/ }

assert_not_nil elements.detect \
{ |e| e.to_s =~ /#{Regexp.escape(amount)}/ }

end

Things to watch out for:
  • You need to force the conversion of the element to a String for the regular expression comparison (with to_s)
  • In this particular example, since I am using string literals with special characters, I had to use Regexp.escape to make $ and . match properly

Labels: ,