|
Peter Marklund's Home |
Test Driven Development with Ruby
I am closing my DreamHost account, partly because they have security issues and partly because I just don't need it anymore. I had to move my Test Driven Development with Ruby article to a new home here at marklunds.com. Hopefully Google will pick up the new page and update its index soon.
Modularizing Your Rails App with Concerns
In his keynote here at RailsConf Europe 2008, David (DHH) talked about living with legacy code, how we should enjoy it instead of trying to avoid it, and how it can give us new insights by showing us how we have grown as developers. I loved the keynote and it resonated really well with my own experiences. It's also highly relevant with my current work at Newsdesk and Simple Signup.
As an example of refactorings you might find yourself doing in legacy Rails apps, David showed us how to break a big application helper or a fat model into Ruby modules. The idea was to find groups of methods that represent a certain concern or aspect of your app and collect those in a module. This is typically not done for reuse but to make your code more readable and easier to navigate.
Last week I found myself creating a plugin for a certain aspect of my application, namely the acceptance of terms of its service. It was a minimalist plugin with just a helper method and a controller filter method. The code was highly application specific and thus it felt wrong to keep it in the plugin directory. After all, plugins are supposed to be shared across applications and my plugin was inherently tied to the application. Still, I wanted to have the ability to keep an aspect of my application in its own directory, especially when the aspect spans across several layers of MVC. The solution I came up with was to create a new directory RAILS_ROOT/app/concerns with a sub directory and an init.rb file for each concern, very much like with plugins. I then generated a concerns plugin to make sure my concerns get loaded:
Dir[File.join(RAILS_ROOT, "app", "concerns", "*", "init.rb")].each do |init_file| require init_file end
I talked to David (DHH) about this approach and the funny thing was that he had experiemented with it too. David says the approach can be appropriate if you use it wisely. If you overuse it your concerns directory will end up being a new "garbage can" and bring you back to the problem that you were trying to solve with concerns in the first place... :-)
Introducing Rails Mentor
Me and fellow Rails developer Carl-Johan Kihlbom from Gothenburg have just founded Rails Mentor. Rails Mentor is a network of Ruby on Rails experts and we offer mentorship and training in anything related to Ruby on Rails. Me and Carl-Johan are committed to Rails best practices and together we have a broad experience of applying Ruby on Rails to varying types of projects. Now we would like to help spread this knowledge to others. If you need to be brought up to speed quickly with Rails or need a code or architecture review, please don't hesitate to get in touch with us.
We are currently at RailsConf in Berlin and we are giving free Rails Mentor t-shirts so come talk to us if you want one!
Installing Ruby on Rails on Ubuntu 8.04 Hardy Heron
I've compiled a set of detailed instructions on how to install Ruby on Rails on Ubuntu 8.04 Hardy Heron. The instructions are available in GitHub and they show you how to turn a clean Ubuntu 8.04 install into a production ready Ruby on Rails stack including MySQL, Nginx with fair proxy balancer, Monit, and Mongrel Cluster. There are a few additions and improvements I'd like to make:
- A more recent version of Ruby
- Logfile rotation
- Production log analyzer
- PostgreSQL
Please help me point what else is missing or how I can improve.
I'm talking to GleSYS about the possibility of offering my production setup as an installable VPS image.
Faster Capistrano Subversion Deployments with remote_cache
This is old news, but if you are deploying an application with Capistrano from Subversion and you find yourself waiting impatiently for the checkout to happen everytime, try adding this to deploy.rb:
set :deploy_via, :remote_cache
With this setting Capistrano will keep a Subversion checkout under shared/cached-copy, and on each deploy just do an svn update followed by a local copy of the whole tree into releases. I found this to be significantly faster than the default checkout on each deploy.
I am planning to switch to Git and GitHub now, and I am curious to see if this setting will affect deployment speed with Git. Given that a git clone in my experience is blazingly fast, maybe the remote cache won't be needed?
Installing Telenor 3g Modem (Mobilt Bredband) on Ubuntu
As far as I know none of the turbo 3g GSM modems on the swedish market officially support Linux. However, I was able to get my Telenor modem working just fine on my Ubuntu Eee just now. I used the USB_ModeSwitch software along with swedish instructions from Hasain. Once my modem was recognized I ran sudo wvdialconf. I then edited my /etc/wvdial.conf to be:
[Dialer Defaults] #Init1 = ATZ Init1 = AT+CPIN=<YOUR PIN HERE> Init2 = ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0 Modem Type = Analog Modem Baud = 9600 New PPPD = yes Modem = /dev/ttyUSB0 ISDN = 0 Phone = *99# Password = peter Username = peter
Note that you have to change the PIN above. I then ran sudo wvdial and voila - I was online! Well, online on a slow and unreliable connection that is. Oh well, I'm thinking of changing to tre.se one of these days. They supposedly have the best 3g network.
Asus Eee + Ubuntu + Rails
On thursday I bought myself an Asus Eee 900 - a tiny and cheap Linux powered laptop that is currently selling out in stores here in Sweden. I got a lot of attention in the office with this laptop and within a day it seemed every programmer in the office had an Eee on their desk.
I installed Ubuntu Eee and this was a huge improvement over the Linux OS that Asus provides. I am ashamed to admit that I haven't used Linux on the desktop for a long time and I was totally blown away by how advanced, slick, and user friendly Ubuntu has gotten.
Obviously, my ultimate goal was to install Ruby on Rails. At first I wanted to install from source in order to get an exact version and patch level of Ruby, namely the one that is officially recommended on ruby-lang.org. However, when attempting this various libraries were missing. I found a FiveRuns article listing the packages I needed but after I had installed them I ran into an issue with the MD5 library. In the end I resorted to using the ruby-full package which gives you the old tried and tested Ruby 1.8.6 patch level 111 (without the recent security patches). Here, roughly, are the steps I went through to set up my Rails environment:
##################################### # RUBY ##################################### sudo apt-get install ruby-full which ruby ruby -v # => ruby 1.8.6 (2007-09-24 patchlevel 111) [i486-linux] ruby -ropenssl -rzlib -rreadline -e "puts :success" ##################################### # RUBYGEMS ##################################### # Get latest stable recommended release of RubyGems from rubygems.org wget http://rubyforge.org/frs/download.php/38646/rubygems-1.2.0.tgz tar xzf rubygems-1.2.0.tgz cd rubygems-1.2.0/ sudo ruby setup.rb # Not sure if/why this step is necessary sudo ln -s /usr/bin/gem1.8 /usr/bin/gem which gem gem --version # => 1.2.0 ##################################### # MYSQL ##################################### sudo aptitude install mysql-server mysql-client libmysqlclient15-dev ##################################### # Some useful Gems ##################################### sudo gem install rails mongrel capistrano mysql
I haven't double checked those instructions for accuracy since I would have to re-install my OS to do that. If you find errors or have improvements, please let me know. Here is a sample of articles on the subject that you might want to check out if you want to dig deeper:
Rails Configuration: Yielding self in initializer
I've come across situations in Rails where you want to repeatedly invoke methods on some class with a long name and it gets ugly and tedious that you have to repeat the class name on every line. I just realized that unlike in the config/environments files the config object is not available in the config/initializer files. I use the AppConfig plugin to parameterize my application and I came up with the yield_self method to make my config/intializers/app_config.rb more readable:
# Method to supplement the app_config plugin. I want to crash early and be alerted if
# I have forgotten to define a parameter in an environment.
AppConfig.param(name) do
raise("No app_config param '' defined in environment , " +
"please define it in config/environments/.rb and restart the server")
end
end
yield self
end
end
AppConfig::Base.yield_self do |config|
config.site_name = "Simple Signup"
config.admin_email = '"Simple Signup" <info@simplesignup.se>'
config.exception_emails = %w(info@simplesignup.se)
config.email_prefix = "[]"
config.signup_timeout = 5
config.send_activate_email = false
config.transaction_fee = lambda do |price|
price.blank? ? 0.0 : [10.0, 5.0+0.035*price.to_f].max.round(2)
end
config.bank_fee = lambda do |price, card|
0.0
end
end
ExceptionNotifier.exception_recipients = config_param(:exception_emails)
ExceptionNotifier.sender_address = %{peter_marklund@fastmail.fm}
ExceptionNotifier.email_prefix = " ERROR: "
The method config_param might be more appropriately named app_config. That will be a refactoring for another day though.
Ruby Gotcha: Default Values for the options Argument Hash
Like in Java, method arguments in Ruby are passed by reference rather than by copy. This means it's possible for the method to modify the arguments that it receives. This can happen unintentionally and be a very unpleasant surprise for the caller. A good example of this is the typical options = {} at the end of the method argument list. If you set a default value in that hash then that is a potential issue for the caller when the options hash is reused in subsequent calls (i.e. in a loop). See below for an example:
options[:to_table] ||= from_column.to_s[/^(.+)_id$/, 1].pluralize
execute ["alter table ",
"add constraint ",
"foreign key ()",
"references (id)",
""].join(" ")
end
from_columns.each do |from_column|
foreign_key(from_table, from_column, options)
end
end
In the first invocation of foreign_key options[:to_table] will be set (if it isn't set already) to the destination table of the first column. The options[:to_table] value will be retained throughout the loop causing all foreign keys to point to the same table. The fix is to make to_table a local variable or to do add an "options = options.dup" line at the beginning of the method.
Lesson learned - avoid modifying the options hash and any other arguments that the caller doesn't expect will be modified.
Rails Testing Tip: Use Global Fixtures to Avoid Fixture Mayhem
I'm in a Rails team with mixed opinions on whether to use fixtures. Therefore we have everything from RSpec specifications that use a lot of mocking/stubbing and don't touch the database, to specifications that set up their own databse data through helper methods and the specifications that I write that rely mostly on fixture data. What I have found is that when you don't use global fixtures (a setting in your test_helper.rb or spec_helper.rb file) you can run into situations where seemingly unrelated specifications/tests fail, and fail in different ways depending on if you run them in separation, through autotest, or with rake. What is going on is test data spillover/interference between tests. This can lead to very long and frustrating debugging sessions indeed. The best way to avoid this seems to be to turn on global fixtures. This will probably increase the specification run time, an issue that I partially adress by keeping the number of records in my fixture files to a minimum. Also, I prioritize test coverage and convenient access to a common set of test data over making my specifications run faster.
Rails Optimistic Locking - Not Worth it for Me
When I upgraded to Rails 2.1 ActiveRecord partial updates were turned on, i.e. when you save a record only those attributes that have changed are saved to the database. In theory, if you have two almost simulateneous updates, and you have a validation rule across several columns, then those updates can render the database record in an invalid state. Of course, in practice, this is very unlikely to happen. Nevertheless, I decided to turn on optimistic locking to be on the safe side. It turned out optimistic locking caused more issues than it was worth. Suppose you have an Article model that has many instances of Chapter and also that the Article uses a counter cache. Then you can run into this issue:
article = Article.first
article.chapters.create(:name => "Summary")
# => UPDATE articles SET chapters_count = chapters_count + 1,
# lock_version = lock_version + 1 WHERE id = XXX;
article.publish_date = 1.days.from_now
article.save
# => throws ActiveRecord::StaleObjectError
I like partial updates though since they make it less likely that simulteneous updates will clobber, since you are only writing to the db what has changed. It also makes SQL statements in the log file more readable.
I'm abandoning optimistic locking for now though.
Ruby Gotcha: Symlinked Scripts and File.dirname(__FILE__)
If you have a Ruby script say in ~/src/ruby/my_script that you are symlinking to from ~/bin/my_script, then invoking File.dirname(__FILE__) in that script will yield the directory of the symlink not the directory of the script file. If you want the directory of the script file you can do this instead:
THIS_FILE = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
THIS_FILE will contain the path to the script file instead of the path to the symlink. This is valuable if say you want to require some Ruby library from your script and you are using a relative path.
Rails Tip: Validating Option Arguments
I think it's a good convention to validate options arguments passed to methods, i.e. to make sure they have valid keys and values. Misspellings can otherwise lead to unnecessary debugging sessions. Rails comes with the assert_valid_keys method. I added the assert_value method:
#:nodoc:
#:nodoc:
#:nodoc:
# Assert that option with given key is in list
options.assert_valid_keys([:in])
if !(options[:in].map(&:to_s) + ['']).include?(self[key].to_s)
raise(ArgumentError, "Invalid value '' for option '' must be one of ''")
end
end
end
end
end
end
Example usage and RSpec specification:
options.assert_valid_keys([:billing_period])
options.assert_value(:billing_period, :in => ["annual", "monthly"])
# method logic here...
end
# Rspec:
it "cannot be invoked with an invalid option" do
lambda { @campaign.price([23], :foobar => true)
}.should raise_error(ArgumentError)
end
it "cannot be invoked with an invalid billing period" do
lambda { @campaign.price([23], :billing_period => :foobar)
}.should raise_error(ArgumentError)
end
Rails plugin: acts_as_state_machine_hacked
I've been using the acts_as_state_machine plugin in a couple of projects. I think the syntax and functionality of the plugin is quite nice. It allows you to easily define states and events (transitions between states) for your ActiveRecord model. However, I wanted to be able to see which states are available in the current state. Also I thought that invoking an event, such as user.activate!, when the user is in a state where the activate event is not available should not yield a silent failure, but rather throw an exception. Also, if the event fails to fire because of a guard condition then an exception should also be raised. I encapsulated those changes in the plugin acts_as_state_machine_hacked.
DreamHost Hacked: All My Files Exposed Publicly
An ex-colleague of mine discovered that all my files in my home directory at the hosting company DreamHost were publicly viewable and downloadable on the web. I was quite shocked. I had certainly not intended to share all my private files with the world, especially since they contained some highly sensitive information. I assumed my account at DreamHost had been hacked. However the response from DreamHost support was that this was not the case. They explained that it was merely a symbolic link to my home directory that had been created:
"if you would like to keep this from happening you can prevent all other users on the server from viewing your account's files by enabling the Enhanced Security feature for your user. Just go to the Users > Manage Users section of your panel, click the "Edit" link next to your user, and then check to enable the Enhanced Security option. Hit the "Save Changes" button and you should be set in about 20 minutes.
The /home/ directory is public and it is not a security breach that the other user was able to create a symbolic link to /home/. Other users on the server have always had the same access, which means that they have been able to view your files but they absolutely cannot make any changes to your files or folders. The Enhanced Security feature takes it a step further and prevents any user from even viewing your files or folders.
So, just to be clear, there is no indication of a server hack or any security intrusion."
I wrote back that I had changed to "Enhanced" security and that my files were still exposed. Here are some excerpts from their second reply:
"Ultimately this was just some funny permissions on your home directory which caused this to be allowed to happen."
"When I changed your home directory's group ownership back to your default group (pg136611) this corrected the insecurity of other user's accessing your files via apache"
"The interesting part is it may have been enabling the extra web security which caused this insecurity."
A few days later I found that my files were still exposed and I had to manually change the group of my home directory. Basically as far as I'm concerned this means the issue has still not been fixed in a reliable fashion.
I've heard no apology from DreamHost so far. In fact, there is not much in their replies that indicates that they are even taking the issue very seriously. I'm quite disappointed and I am not left with much confidence in DreamHost when it comes to security and privacy.
Rails Testing Tip: Validate your Fixtures
I realized today how important it can be to validate the fixtures you use in your Test::Unit tests or RSpec specifications. I made some schema and validation changes and neglected to update all my fixtures which lead to a long and tedious debugging session. I added this RSpec specification to make sure I never have invalid fixtures again:
describe "fixtures" do
it "should be valid" do
ActiveRecord::Base.fixture_tables.each do |table_name|
klass = table_name.to_s.classify.constantize
klass.send(:find, :all).each do |object|
puts(" is invalid: ") if !object.valid?
object.should be_valid
end
end
end
end
Note: the fixtures_tables method is just a method I have defined that returns a list of all my fixture tables and I use it to set global fixtures in my test_helper.rb and spec_helper.rb files. If you are not using global fixtures, you can use this spec instead:
describe "fixtures" do
it "should be valid" do
Fixtures.create_fixtures(fixture_path, all_fixture_tables)
all_fixture_tables.each do |table_name|
begin
klass = table_name.to_s.classify.constantize
klass.send(:find, :all).each do |object|
puts(" is invalid: ") if !object.valid?
object.should be_valid
end
rescue NameError
# Probably a has and belongs to many mapping table with no ActiveRecord model
end
end
end
Spec::Runner.configuration.fixture_path
end
Dir[File.join(fixture_path, "*.yml")].map {|file| File.basename(file[/^(.+)\.[^.]+?$/, 1]) }
end
end
I think it would be nice if Rails/RSpec has fixture validation built in and turned on by default.
Rails Tip: Validating Option Arguments in your Methods
I think it's a good convention to validate that options passed to methods have valid keys and values. Misspellings can otherwise lead to unnecessary debugging sessions. Rails comes with the Hash#assert_valid_keys method. I added the assert_value method:
#:nodoc:
#:nodoc:
#:nodoc:
# Assert that option with given key is in list
options.assert_valid_keys([:in])
if !(options[:in].map(&:to_s) + ['']).include?(self[key].to_s)
raise(ArgumentError, "Invalid value '' for option '' must be one of ''")
end
end
end
end
end
end
Example usage:
options.assert_valid_keys([:billing_period])
options.assert_value(:billing_period, :in => ["annual", "monthly"])
...
Corresponding RSpec specifications:
it "cannot be invoked with an invalid option" do
lambda { @campaign.price([23], :foobar => true)
}.should raise_error(ArgumentError)
end
it "cannot be invoked with an invalid billing period" do
lambda { @campaign.price([23], :billing_period => :foobar)
}.should raise_error(ArgumentError)
end
RSpec Presentation
I gave a presentation on RSpec today at Diino.com - an online backup provider - and the slides are available here.



