Monday, February 18, 2008
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: rails, rspec, testing
Wednesday, January 23, 2008
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: autotest, testing
Wednesday, January 16, 2008
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: autotest, rails, rspec, testing
Sunday, August 26, 2007
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: rails, testing
Wednesday, May 30, 2007
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: rails, testing
Sunday, May 20, 2007
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: rails, testing
Sunday, April 22, 2007
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: rails, testing