|
Peter Marklund's Home |
Peter on Rails
Lessons Learned in Ruby on Rails and related technologies.
Subscribe
Rails Testing: To Stub or Not to Stub
Over the last couple of years testing has been a controversial topic in Rails. In the beginning though Rails was of course opinionated and gave us model and controller tests along with fixtures for testing our apps. Then integration tests were added by Jamis. Some started using browser testing with tools such as Selenium and Watir. Then the whole RSpec movement swept in with a new terminology, a heavier use of mocking and stubbing, and more isolated testing of the different layers of the MVC stack. One of the most important trends right now seems to be to do "Outside In TDD" with Cucumber and webrat. There are alternatives to RSpec like Shoulda and a move towards simpler tools such as Jeremmy Mcannalys Context and Matchy libraries. There are a number of Factory libraries for replacing fixtures. Maybe the most important controversy over the years has been on whether the database should be stubbed out or not. One of the most common arguments for stubbing out the database is to keep the test execution time low.
Personally I've always been a stubbing sceptic. Given all the changes in the testing landscape it's interesting to read that at Thoughtworks there is a movement away from stubbing and back to where it all started:
"As the teams became familiar with using method stubbing, they used it more and more - falling into the inevitable over-usage where unit tests would stub out every method other than the one being tested. The problem here, as often with using doubles, is brittle tests. As you change the behavior of the application, you also have to change lots of doubles that are mimicking the old behavior. This over-usage has led both teams to move away from stubbed unit tests and to use more rails-style functional tests with direct database access."
Rails Development Database Setup Without Migrations
I posted over at the Newsdesk developer blog about Rails Development Database Setup Without Migrations.
The Newsdesk Developer Blog
I have been doing a bit of blogging over at the Newsdesk developer blog. My two most recent posts are on shelling out to external programs and eager loading of application classes to save STI.
Rails Gotcha: ActiveRecord##Base.valid?, errors.empty?, and before_validation Callbacks
The ActiveRecord::Callbacks module (that depends on ActiveSupport::Callbacks) defines a multitude of before and after callbacks for the lifecycle of your ActiveRecord objects. Similar to how before filters in controllers work, if a before_validation callback method returns false, the save process will be aborted. Here is what the API documentation says:
# If the returning value of a +before_validation+ callback can be evaluated to +false+, # the process will be aborted and Base#save< will return +false+. # If Base#save! is called it will raise a ActiveRecord::RecordInvalid exception. # Nothing will be appended to the errors object. # If a before_* callback returns +false+, all the later callbacks and the associated # action are cancelled. If an after_* callback returns # +false+, all the later callbacks are cancelled. Callbacks are generally run in the # order they are defined, with the exception of callbacks # defined as methods on the model, which are called last.
What this means is that if a before_validation callback returns false, then save and valid? will both return false but errors.empty? will return true. Yes, you read correctly, there are no validation errors but the record is not valid. Also, note that the valid? method defined in the ActiveRecord::Base class will never even be invoked.
Rails Tip: Running Tests with Verbose Output
If you want to run your Rails tests with verbose output you can use the TESTOPTS argument like this:
rake test TESTOPTS="-v"
Plugin Stack Traces Missing in Rails 2.3.2
I endured a nightmarish debugging session yesterday due to Rails no longer showing proper stack traces from exceptions in plugin code. I've verified this across different Rails apps and it seems to actually be the case that with Rails 2.3.2 you no longer get a stack trace from plugins. Has anybody else had issues with this? Any pointers to where in the Rails code exception handling and stack traces are dealt with and how it could be fixed? I posted about this issue on the Rails core list but haven't received a reply yet.
You're a SlideShare RockStar
SlideShare sent me an email saying I'm a rockstar because my Ruby on Rails 101 slides have been getting so popular. I'm really glad that I decided to share my slides and SlideShare has been a great boost in reaching more people. All the great feedback that I get from people makes all the work that went into the slides more than worthwhile. I continue to be fascinated with how rewarding it can be to share information on the web.
Rails Tip: Using Rakismet to Stop Spam
I am trying out Rakismet now for this blog to prevent comment spam - an issue that has been bugging me for a long time and that has been getting worse lately. Setting up the Rakismet plugin was a breeze. Very nice! Now, let's hope it actually stops the spammers. We use Rakismet at Newsdesk and I think it's worked out really well there.
Really Simple Rails Log Rotatation
I always used logrotate Linux tool to setup log rotation for my Rails apps which has worked fine although it required finding some external config file and understanding its config options and syntax. I never new log rotation could be set up by adding this one line to config/environments/production.rb right in your app:
config.logger = Logger.new(config.log_path, 50, 1.megabyte)
Sweet!
Upgrading from Rails 1.2.3 to Rails 2.3.2
Have you ever tried upgrading Rails from version 1 to version 2? If not, you're in for a treat. Seriously though, it's not that bad, but depending on the size of your app, the number of plugins you use, and how badly you've patched Rails, your mileage may vary.
Here are some notes from an upgrade of this little weblog app just now:
Contributing to Rails Core
At Newsdesk we recently upgraded to Rails 2.3 and ran into a Rails bug triggered by multiple post requests in tests. I was curious to figure out why the bug occured and since I was already familiar with the Rails testing code (in test_process.rb) I was able to nail down the problem pretty quickly. Usually I would probably stopped there, but my colleague Richard encouraged me to submit a Rails patch so I walked the extra mile and wrote a little unit test for my fix. I then followed the contribution instructions to submit an issue in Lighthouse with a patch file followed by the recommended post to the Rails core mailing list. The issue was quickly assigned to Joshua Peek in the Rails core team and a few days later it was applied. It is really encouraging to see how easy it can be to contribute to Rails and that the core team actually picks up a lot of patches and applies them if only they are submitted properly. This really illustrates the power of of open source in general and Git and Rails in particular.
Rails Tip: JavaScript Validation and Testing
JavaScript test coverage is pretty non-existant in most Rails projects and if your application has a lot of JavaScript code this can become a real problem. But it doesn't have to be that way. One thing you can do just to get some basic syntax and style checking of your JavaScript code is to run it through JavaScript Lint. I've added a simple rake task to run all javascript files through the validator:
namespace :test do
namespace :javascripts do
desc "Validate all javascript files with javascript lint - assumes jsl on the command line"
# Visit http://www.javascriptlint.com to download and install the jsl command line tool
task :validate do
total_errors = 0
Dir[File.join(File.dirname(__FILE__), "..", "..", "public", "javascripts", "*.js")].each do |file_path|
print " ... "
result = `jsl -process `
n_errors, n_warnings = result.match(/(\d+) error\(s\), (\d+) warning\(s\)$/).to_a[1, 2].map(&:to_i)
if n_errors > 0
puts "FAILED"
puts result
else
puts "OK ( warnings)"
end
total_errors += n_errors
end
print "TEST RESULT: "
if total_errors > 0
puts "FAILURE - errors"
else
puts "OK"
end
end
end
end
In addition to syntax checking I recommend trying out Dr Nic's javascript_test plugin. To get it to work with the latest prototype I had to download JsUnitTest and use that instead of unittest.js.
Rails Tip: Migrate Your Database to UTC
UPDATE: Adam Meehan pointed out that my migration didn't work with DST, i.e. different UTC offsets for datetimes at different points of the year. I updated my migration to use a UTC conversion in the database (leading to a variable interval) instead of using a fixed interval adjustment.
If you want to make use of the timezone support in Rails 2.1 and later you'll need to migrate any existing times that you have in your db to UTC. Here is a migration for PostgreSQL I wrote to do that (you'll probably need to adjust it to work on MySQL):
# This migration will work with DST. Because of DST, if you have your datetimes
# spread across the year they will have different offset, i.e. in Stockholm we are UTC+1 usually
# but UTC+2 in the summer.
end
When I originally wrote this migration I used a fixed interval adjustment (+1 for Stockholm), i.e. the same approach that Simon Harris uses. However, as Adam Meehan points out in the comments this doesn't work with DST so I adjusted to using a AT TIME ZONE 'UTC' conversion instead that will result in a DST dependent interval. Thanks Adam for pointing this out!
Ruby on Rails 101 Slides Updated for Course in Italy
Last week I had the pleasure of teaching a five day introductory course at Tiscali in Cagliari/Itali (Sardinia). The course outline was similar to the course I held back in 2007 but in this case the lectures were half day with the afternoons dedicated to exercises and questions. This format worked out pretty well and I think it's a good idea to have half of the time devoted to hands-on training.
I've updated my old slides for Rails 2.3 and rewritten them in HTML (using S5/Codex). You can view the slides here. The source code for the slides is available on GitHub. Feel free to send feedback and reuse the slides and make any corrections and improvements that you see fit. Thanks!
Translate: New I18n Rails Plugin with Nice Web UI
Today Newsdesk released the Translate plugin that provides a nice web UI for doing translations. The plugin mounts a web UI at /translate where you can list and translate I18n texts. Translations are written directly to YAML files stored at the default location under config/locales. Check out the post at the Newsdesk blog and the README file on Github for more details.
Rails Timezone Gotcha: ActiveRecord::Base.find does not convert Time objects to UTC
With the timezone support introduced in Rails 2.1 the idea is that all dates in the database are stored in UTC and all dates in Ruby are in a local timezone. The local timezone can be specified by config.timezone in environment.rb or set to the user timezone with Time.zone= in a before filter. Typicaly, when reading/writing from/to the database ActiveRecord will transparently convert time attributes back and forth to UTC for you. However, there is a gotcha with datetimes in ActiveRecord::Base.find conditions. They will only be converted to UTC for you if they are ActiveSupport::TimeWithZone objects, not if they are Time objects. This means that you are fine if you use Time.zone.now, 1.days.ago, or Time.parse("2008-12-23").utc, but not if you use Time.now or Time.parse("2008-12-23"). Example:
Apparently this issue has been reported and marked as invalid. I think it's quite unfortunate that ActiveRecord doesn't do this conversion for us. I suspect other application developers will be bitten by this as well. The difference in behaviour between Time and TimeWithZone objects boils down to the to_s(:db) call:
>> Time.now.to_s(:db) Time.now.to_s(:db) => "2009-01-06 17:52:19" >> Time.zone.now.to_s(:db) Time.zone.now.to_s(:db) => "2009-01-06 16:52:23"
One way to fix the issue would be to monkey patch the Quoting module in ActiveRecord like this:
# :nodoc:
# Convert dates and times to UTC so that the following two will be equivalent:
# Event.all(:conditions => ["start_time > ?", Time.zone.now])
# Event.all(:conditions => ["start_time > ?", Time.now])
value.respond_to?(:utc) ? value.utc.to_s(:db) : value.to_s(:db)
end
end
end
end
However I'm not sure that this is a good idea and that it won't break anything else. I've at least verified that it doesn't break assignment of ActiveRecord attributes.
Introducing Simple Signup - Easier Event Signups and Payments
Simple Signup is a web application that I've been working on for a while now that makes it easier for event organizers to accept signups and payments. Bodil Abelsson, who is now my business partner, approached me about a year ago with the idea and it made sense to me right away, especially since I had needed a service like this myself. If you think about it, being able to accept signups and payments is quite a common need in everything from private parties, concerts, sports, conferences, courses, and club memberships. Maintaining attendee lists and matching them up with payments made to your bank account can be quite a hassle. It is time consuming and error prone. One would expect there to be any number of established websites by now that solve this problem. However, there really isn't. Many small organizers still handle payments and signups manually. Some of the medium size organizers have systems that they have developed themselves and that are specialized for their niche. However there aren't that many services out there that recognize the generic nature of the problem. There is some competition in Germany and the US but we seem to be the first service of this kind in Sweden.
The swedish version of Simple Signup (simplesignup.se) was launched quietly this summer. This was followed by a recent launch of the english version (simpleeventsignup.com) along with the ability to accept payments via Paypal. Swedish event organizers don't need a Paypal account but instead typically choose to have payments handled by Simple Signup. This works by having the attendee pay us via our Payment Provider Payex by credit card (VISA or MasterCard). We then transfer the money directly to the organizers bank account. To be able to cover credit card and bank fees we charge a 4% signup fee (at least 10 SEK).
We are continually improving the site and have plans to partner up with other websites that provide services related to events. I'll have more to say about this soon. One of the bigger and more important features that we will be adding is the ability for the event organizer to design their own signup page.
We still have a long way to go, but at least I think we are off to a good start. If you have any feedback, positive or negative, it is always greatly appreciated.
MySQL Performance
Every now and then I like to pick on MySQL and it's become something of a running theme in this blog. My session table for this blog (which runs on Ruby on Rails of course) had grown too big so I needed to clean it up. I just didn't expect it to take this long:
mysql> select count(*) from sessions; +----------+ | count(*) | +----------+ | 545797 | +----------+ 1 row in set (2.89 sec) mysql> delete from sessions; Query OK, 545801 rows affected (5 min 46.67 sec)
Should it really take 3 seconds to count half a million rows? I wonder if PostgreSQL would deliver better performance in this instance. As soon as I find the time I will switch over my Rails/MySQL based web application Simple Signup to PostgreSQL. I have a strong personal preference for PostgreSQL over MySQL. I'm curious to see what the transition will be like though.
Rails Tip: SEO Friendly URLs (Permalinks)
There are several plugins already out there that can turn the typical Rails show path of /articles/show/1 into something more search engine friendly. I decided to roll my own implementation and it turned out to be fairly easy. My solution relies on the Slugalizer library by Henrik Nyh. First, I make sure we can turn any string into something URL friendly by patching the String class:
# Convert String to something URL and filename friendly
no_slashes = self.gsub(%r{[/]+}, separator)
Slugalizer.slugalize(no_slashes.swedish_sanitation, separator).truncate_to_last_word(max_size, separator)
end
# We need this for SEO/Google reasons sincen å should become aa and Slugalizer translates å to a.
dup = self.dup
dup.gsub!('å', 'aa')
dup.gsub!('Å', 'aa')
dup.gsub!('ä', 'ae')
dup.gsub!('Ä', 'ae')
dup.gsub!('ö', 'oe')
dup.gsub!('Ö', 'oe')
dup.gsub!('é', 'e')
dup.gsub!('É', 'e')
dup
end
dup = self.dup
if dup.size > length
truncated = dup[0..(length-1)]
if truncated.include?(separator)
truncated[/^(.+)/, 1]
else
truncated
end
else
dup
end
end
end
All I have to do in my ActiveRecord model then is to override the to_param method:
if name.present?
"-"
else
id
end
end
permalink
end
ActiveRecord will automatically ignore any non-digit characters after the leading digits in an id that you pass to it, but just to be on the safe side I added a before_filter to my application controller that will convert permalinks to proper ids:
if params[:id].present? && params[:id] =~ /\D/
params[:id] = params[:id][/^(\d+)/, 1]
end
end
Credit for parts of this code goes to my cool programmer colleagues over at Newsdesk.se.
Rails Hack: Auto Strip ActiveRecord Attributes
We have a user who unintentially enters a space after his email address. It seems that a lot of times it makes sense to automatically strip ActiveRecord model attributes before they are validated. Inspired by this post I came out with my own auto_strip method that adds a before validation callback which seems less intrusive than redefining the attribute setter method:
# Sometimes users accidentally enter space before or after text in text fields. Let's not punish them
# with an error message for this.
attributes.each do |attribute|
before_validation do |record|
record.send("=", record.send("_before_type_cast").to_s.strip) if record.send(attribute)
end
end
end
end
end
Now in my ActiveRecord model I can say for example:
Maybe auto stripping would be useful as an option to the Rails validation macros?


