RSpec and HAML Helpers
Today I faced a problem with specing a helper where I use the haml_tag/puts methods (haml_tag was previously open).
Here’s a solution working with Rails 2.0.2, RSpec 1.0.3 and HAML 1.8.2 :
in spec_helper.rb add :
Spec::Runner.configure do |config|
...
# Activate haml to spec helpers
config.with_options(:behaviour_type => :helpers) do |config|
config.include Haml::Helpers
config.include ActionView::Helpers
config.prepend_before :all do
# Update from Evgeny comment, with HAML >= 1.8.2, you can call
init_haml_helpers
# Old way for HAML <= 1.8.2
# @haml_is_haml = true
# @haml_stack = [Haml::Buffer.new]
end
end
...
endthen you can write your helper spec like this :
describe ApplicationHelper do
describe "#top_navigation_menu when logged in" do
it "returns the menu" do
@user = mock_model(User)
capture_haml {
top_navigation_menu(@user)
}.should_not be_empty
end
endThe interesting method is capture_haml, which does the same as Rails builtin capture but for HAML.
haml_tag/puts methods write output directly to the buffer and does not return the generated content as a String, thus we cannot just test on the method output.[…]
RSpec 1.1
David Chelimsky anounced this morning the release of RSpec 1.1 (as of now the website is not up to date) RSpec 1.1 and just now the brand new website hosting.
What’s in this release ?
- StoryRunner : merge from Dan North ’s RBehave library. It aimed at providing a clean way to express and automate Acceptance Tests.
- Nested Example Groups : allows you to nest your
describeblocks resulting in group sharing common specifications.
- Support for Rails 2.0.1
- Test::Unit interoperability : switch smoothly from Test::Unit to RSpec by allowing you to run your Test::Unit tests with RSpec. The goal here is to provide a way to progressively transition your tests to RSpec syntax.
More infos on David blog post about the RSpec 1.1 release
Congratulation and many thanks to everyone involved in this release ![…]
RSpec and Inline RJS
Rails give us the ability to write inline RJS via render :update syntax, as in :
render :update do |page|
page['addressPreviewStatus'].update 'Address Not Found'
endPrevious code will update the content of tag with “addressPreviewStatus” id with ‘Address Not Found’.
But how can we spec that out ? I needed to search a little as there seems to be very little examples.
First in rails there is a special assertion assert_select_rjs, merged from the assert_select plugin, it let you test your RJS with a syntax similar to RJS itself.
RSpecOnRails has a special matcher wrapping assert_select_rjs : has_rjs.
You can use it on response to specify what should be generated, for exemple you can :
# Specify response should contains an update or insert of some kind
response.should have_rjs
# Specify response should contains an update or insert for the tag with given id
response.should have_rjs('id')
# Specify response should contains a specific update, insert, etc. for the given tag
response.should have_rjs(:replace, 'id')You get the point. Now, a nice syntax allows you to write RJS this way :
render :update do |page|
page['id'].update('replacement text')
endSo my first try was to use :
response.should have_rjs(:update, 'id', 'replacement text')but this fail miserably with an error Unknown RJS statement type update. I tried different syntax but none worked.
Finally browsing through source of assert_select_rjs I found what I was looking for. When using this syntax you should use one of :chained_replace or :chained_replace_html depending what you want to test :replace or :update.
Now here is the solution :
response.should have_rjs(:chained_replace_html, 'id', 'replacement text')Date, Time and old days
Not so long ago a bug emerged on my current application, a vicious one.
Let’s say you have a user and store its bithday date in database (say MySQL) as a DateTime (yep I know, it’s a little to precise for this kind of data).
Rails conveniently retrieve this field as a Time, but as you probably do not be aware of is the range limitation of Time’s dates. In fact the Time class is often used because it’s faster than Date or DateTime, but the tradeoff is it handle date based on epoch time (01-01-1970), where Date/DateTime use Complex class and as such can handle a broader range of date.
Well, my user’s birthday date was somewhere in 60s and so can’t be handle by a the Time class. But it’s not as simple, Rails fallback to DateTime if parsing MySQL datetime field does not work with Time.parse. This results in an object perfectly valid when using it, the problem arise when you try to update the corresponding record in database.
Remember my initial datetime field was fetched as a DateTime ruby object and as Rails include all fields from the record on update it tries to translate the birthday to something MySQL recognize. ActiveSupport include some convenients methods to do that : Time#to_s and Date#to_s takes a parameter allowing us to do something like Time.today.to_s(:db) as well as some convenient Date to Time conversion. Unfortunately, no out of range detection is done in Date#to_time which leads me to the first headache. Then ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS hash does not contains a :db key so calling Date#to_s(:db) results in an erroneous string for the db. (this is corrected in rails edge but not in 1.2-stable branch).
All in all :
- be aware of Time class limitation
- ruby open classes are wonderful : just drop this code in your lib directory and that should make it until rails is patched.
module ActiveSupport
module CoreExtensions
module Date
module Conversions
# Add db and rfc822 format
DATE_FORMATS.merge!({
:db => "%Y-%m-%d %H:%M:%S",
:rfc822 => "%a, %d %b %Y %H:%M:%S %z"
})
end
end
end
end Gettext and Rails
I encountered some problems with gettext rails integration recently, so here they are with explanation and resolution (if I found it).
Localization of ActiveRecordHelper error messages :
I choose to change the error message as explained by the gettext tutorial :
in application.rb
ActionView::Helpers::ActiveRecordHelper::L10n.set_error_message_title(Nn_("An error is occured", "%{num} errors are occured"))
ActionView::Helpers::ActiveRecordHelper::L10n.set_error_message_explanation(Nn_("The error is:", "The errors are:"))this works great, the error message displayed by the helper is the good one… except it only appears in the base language (English in my case). And the text never appears in my .pot file. After some searches in the gettext files, I found the problem : the textdomain for the helper is ‘rails’, which is great because you got validations and errors localized by gettext team for free. But it means you can’t set your own message without modifying the rails.pot in gettext package, which is not a solution because you need to maintain your own gettext gem or apply modifications to all you servers installs, and no you can’t install gettext as a rails plugin as gettext gem compile a gettext.so|.dylib on install which depends of your system.
The solution ? I didn’t find any satisfying one, if you force the textdomain to your app’s one then you need to duplicate and update all the rails translations strings from gettext package.
Gettext#updatepo and models with observers :
One of gettext nifty feature is its ability to extract your model columns names for translation. But the implementation cause some trouble when you have declared some observers in your environment.rb.
To accomplish its tasks, gettext first load rails environment. Then each files are parsed by Gettext::ActiveRecordParser#parse where you have :
old_constants = Object.constants
begin
eval(open(file).read, TOPLEVEL_BINDING)
rescue
$stderr.puts _("Ignored '%{file}'. Solve dependencies first.") % {:file => file}
$stderr.puts $!
end
loaded_constants = Object.constants - old_constantsThis code try to catch the constants the current file add to Object.constants and then parse them either as ruby or ActiveRecord subclass. But if you declared an observer in your environment.rb, the model and its observer is already in the Object.constants and are just ignored by the gettext active record parser.
Solution ? Comment the config.observers line in your environment.rb each time you need to run gettext:updatepo task, not really sexy but it works.[…]
How to use a custom RDoc template from a rake task
I just need to do that, use a custom rdoc template to generate a documentation for a Rails project.
My main concern, regarding the different solutions I found on the web, was I didn’t want to have a template in the ruby installation directory but one that resides in a subdirectory of my rails application.
In fact it’s not a problem … when you find the good command. Here’s my rake task to generate the documentation (I put the Jamis Buck customized template in RAILS_ROOT/doc/rdoc_template/jamis.rb)
namespace :doc do
desc "Generate documentation for the application"
Rake::RDocTask.new("app_jamis") { |rdoc|
rdoc.rdoc_dir = 'doc/app_jamis'
rdoc.title = "My Website Documentation"
rdoc.template = "doc/rdoc_template/jamis.rb"
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('doc/README_FOR_APP')
rdoc.rdoc_files.include('app/**/*.rb')
rdoc.rdoc_files.include('lib/**/*.rb')
}
endThe important part is :
rdoc.template = "doc/rdoc_template/jamis.rb"I tried to use the —template= and -T options in rdoc.options but it didn’t work… then I found that the RDocTask appends its template variable to the options passed to RDoc bypassing mine.[…]
RSpec on Rails : RESTful Authentication
For a new Rail project I’m working on, I decided to try to use RSpec.
The main problem I see with this solution is that not so many plugins out there have RSpec tests and so it can be boring to have to run autotest and rspec_autotest in parralel to be sure both RSpec and Test::Unit tests passes.
Helped by a recent post of Yurii Rashkovskii : RSpec on Rails: acts_as_authenticated , I adapted and add a few specifications to be compatible with restful_authentication plugin.
Here are the files :
- user_spec.rb : User model specs
- users_controller_spec.rb : Users controller specs
- sessions_controller_spec.rb : Sessions controller specs
Just put those in spec/{models,controllers}, copy users.yml fixtures from test/fixtures to spec/fixtures/ and you’re done.
Note I did not spec the UserObserver for now.
[Update]
I refactored a little bit my previous files, add some specs and try to spec user_notifier and user_observer models as well.
- user_spec.rb : User model specs
- user_notifier_spec.rb : UserNotifier model specs
- user_observer_spec.rb : UserObserver model specs
- users_controller_spec.rb : Users controller specs
- sessions_controller_spec.rb : Sessions controller specs
You should also add :
def set_mailer_in_test
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
endto Spec::Rails::EvalContext class in your spec_helper.rb.
I’m waiting for your comments on notifier/observer specs, I didn’t see any examples of them in rspec and I’m not sure of the best way to spec them. For example, UserNotifier is an ActionMailer and resides in models directory, but it acts more like a controller (render views), well you see the point.
[Update]
For those of you who switch to RSpec 1.0.x, Jonathan Linowes has updated my files to make them compatible with the new syntax (thanks). You can grab them at : Rspec 1.0 and Restful Authentication[…]
Capistrano tips
If you use (and you definitely should) Capistrano to handle your Rails application deployement, you should have faced some troubles to handle passwords for svn + database, at least if you’re like me and don’t want to put those production passwords in your subversion repository.
My first concern was to have a different user/password to checkout code from subversion, ssh to the server and connect to the database.
So here’s some tricks which allows you to avoid puting passwords in subversion and use different passwords for each part of the process (ssh, svn, db), put those in your config/deploy.rb.
Use whatever SVN User/Password you want :
[UPDATE] A little update to this part to avoid remote svn user/password to be cached locally.
Old :
set :svn_user, ENV['svn_user'] || "jonathan"
set :svn_password, Proc.new { Capistrano::CLI.password_prompt('SVN Password: ') }
set :repository,
Proc.new { "--username #{svn_user} " +
"--password #{svn_password} " +
"http://your.domain.tld/path/to/svn/#{application}/trunk/" }New :
set :svn_user, ENV['svn_user'] || "jonathan"
set :svn_password, Proc.new { Capistrano::CLI.password_prompt('SVN Password: ') }
set :repository,
Proc.new { "--username #{svn_user} " +
"--password #{svn_password} " +
"--no-auth-cache " +
"http://your.domain.tld/path/to/svn/#{application}/trunk/" }So you can now do things like :
svn_user=your_name cap deployand the svn checkout will use ‘your_name’ as svn username, prompt you for the subversion password (remember, you’re using ssh, so no clear password transit) and then do the checkout normaly.
Generate config/database.yml on-the-fly
Just like passwords, it’s a good idea to avoid having your database configuration in subversion if you’re more than one developer to commit code. So why not generate it locally and put it through our lovely ssh connection, at deployement time ?
desc "After updating code we need to populate a new database.yml"
task :after_update_code, :roles => :app do
require "yaml"
set :production_database_password, proc { Capistrano::CLI.password_prompt("Production database remote Password : ") }
buffer = YAML::load_file('config/database.yml.template')
# get ride of uneeded configurations
buffer.delete('test')
buffer.delete('development')
# Populate production element
buffer['production']['adapter'] = "postgresql"
buffer['production']['database'] = "vms_production"
buffer['production']['username'] = "you_db_username
buffer['production']['password'] = production_database_password
buffer['production']['host'] = "localhost"
put YAML::dump(buffer), "#{release_path}/config/database.yml", :mode => 0664
endThis code uses the config/database.yml.template file (it’s up to you to have one, I don’t think this one is hard :D), to generate a simple database.yml containing only the production database configuration. Additionnaly it prompts you for the database connection password.
Isn’t this great ?[…]
Switch to Apache + mod_proxy + Mongrel
In my quest to find the best rails configuration, I decided to try a new alternative to Apache+FastCGI/Apache+FCGID/Lighttpd+FastCGI.
Everyone seems to be focused on Mongrel combined with Apache 2.2 + mod_proxy_balancer. It looks good but Apache 2.2 isn’t actually in Debian (not even in unstable), and as I choose to switch this blog from a Fedora Core 2 (ooch !) server to another server of mine powered by Debian, it really doesn’t fits my needs.
So I choosed to try a different approach and use Apache 2.0.x + mod_proxy + Mongrel. This solution, gives me the possibility to use mongrel_cluster for process management, even if actually I don’t have multiple mongrel instances, and add a load balancer (Pen/Balance/Pound) if needed.
This setup seems to be sufficient for this blog, which runs the latest Typo (3.99.3).
Let me know if you see any differences with the previous configuration.[…]
Enhanced Localization Plugin for Rails
For a project I had at work, I need to have a website in different languages. I assumed this was a common task nowadays, but I quickly found the limits of current solutions in Rails or at least I didn’t found one which matched my criteras for this project : lightweight, allows texts localizations and more importantly permits URLs localizations.
So I took one of the simpler plugins for localization tasks : Localization from Thomas Fuchs. Then I tweaked it a little bit to make it fits my needs and here comes the result : Localization Enhanced. It allows you to easily translate your texts using simple hashes loaded at startup, have as many languages as you want and have controllers/actions translations as well, all using the standards Rails methods (link_to, url_for, etc.). Additionally, I added a little refinement to ActionView::Helpers::AssetTagHelper#image_tag which allows you to use different images based on the current languages where you need it (images-based menu bar, …), by passing :loc => true as an option to image_tag. This one results in image tag generating an url prefixed by the current language (EN_en, FR_fr, etc.) unless the current language is the default one.
If you want to give it a try here’s the subversion repository url to get it :
http://projects.tron.name/svn/railsplugins/localization_enhanced/
or directly install it with :
./script plugin install http://projects.tron.name/svn/railsplugins/localization_enhanced/
License is the same as the original one : MIT Licence (at least I think it is this one as I didn’t find any mention of one in the source).[…]