Showing posts with label rails. Show all posts
Showing posts with label rails. Show all posts

Thursday, March 4, 2010

Google Spreadsheet as an admin panel for your web app

Today, I would like to share a story with you. A story which led us to discover a nice and simple technique for building admin panels.


The customer's request

Recently, in one of our projects (e-commerce), we've had the following requirement:

In order to edit prices
As an admin
I want to export products to Excel, edit them and upload back


At first it seemed strange to me. Why would we want to edit prices using Excel if it can easily be done using the web admin panel which we created. I asked the customer what's the reason and he came up with a fair argument, that it's much easier to mass-edit prices using formulae or using copy/paste in Excel.

I was convinced at this point, but then Yashke (my teammate) suggested that maybe it's better if we do the same, but with Google Spreadsheets (GS), hopefully saving the time for exporting/importing from Excel. The idea sounded pretty cool, so I asked the customer if it was acceptable and I was given a positive response.

The setup we are trying

As always with this kind of risky situations (new technology) we prepared the simplest solution that could possibly work and exposed it in the admin panel. After some discussions we decided for the following workflow.

  • Admin presses "Generate the spreadsheet" in the web admin panel
  • Then he goes to the google spreadsheet page
  • He edits the prices
  • He goes back to the admin panel
  • He clicks "import prices"

As you can see, we're not relying fully on Google Spreadsheets - it's just a tool for editing the data. We still have the traditional web-based admin panel. So the admin logs in to the admin panel and then he can generate a spreadsheet with some data.

This project is not finished yet, but the first feedback is very positive.

The advantages

Our journey with GS started with a single requirement, but over time we discovered many exciting opportunities, like:

  • mass-editing records
  • sorting
  • search
  • search & replace
  • using formulae
  • using colors
  • charts!
I think that the most important argument is that people are used to spreadsheet applications, so giving them a Google Spreadsheet make them feel at home.

In the project I mentioned above, we use it for the following features:

  • mass-editing prices for product variants (we have about 2000 products, each of them can have more than 300 variants, that's a story for another blog post - how assigning prices can be challenging)
  • selecting which products to show on the main page, in what order etc.
  • managing other kind of collections, like bestseller products
  • displaying orders, so that it can be easily sorted, grouped. It's also useful for any kind of reports.

All of these things would be more difficult if we did it in a traditional web admin panel.

Another thing where Google Spreadsheet could be useful is for rapid prototyping. We already have some code that let us easily take a collection of objects and display them in a worksheet, so reusing it will be very easy.

The drawbacks

There are some drawbacks of course:

1. Speed

Exporting 2000 records to GS takes about 1 minute. The good thing is that they start appearing as soon as you press "Export", so you can see the first rows quickly.
The same problem is with importing. It's slow.

2. Relying on Google

We don't know if Google Spreadsheets will live forever, so relying so hard on it may be risky. We try to keep the GS layer simple, so that at any point we can change the implementation to handle Excel or Resolver One files.


Conclusion

So far, our experience with GS is very positive. As we build many web applications for which the admin panel plays an important role, we already consider using GS in other projects, like social network apps (statistics data) or a web surveys app (collecting and analyzing the survey results).

We use exclusively Rails for our web apps, but the idea of using GS can be applied with any other technology.

Let me know if you have any specific questions regarding the topic of using GS as the admin panel. I will be happy to help you.


Wednesday, May 21, 2008

Story Driven Development

If you are interested in testing and the TDD/BDD movements, then you may find Bryan Helmkamp presentation on "Story Driven Development" worth watching. Bryan explains the differences between unit testing and scenarios. He also shows Webrat, a tool he works on, which makes defining stories in Rails apps very easy.

I like the title that Bryan has chosen for the talk. The name "Story Driven Development" makes it clear that it's a lot about defining requirements, and not only about testing. Additionally, I think it's clear that it focuses on the acceptance level of tests without mentioning unit tests.

He also provides a quote from Robert Martin:

Unit tests: “Make sure you write the code right.”
Scenarios: “Make sure you write the right code.”

You can watch the slides here, or grab the PDF version (which has correct code examples).

Wednesday, April 2, 2008

BDD examples with user stories and Webrat

This is the best BDD explanation I have ever seen. It explains all the philosophy behind BDD, shows how it fits with RSpec stories, and how you can use Webrat to create a high-level integration test. It even shows how to use Selenium with RSpec stories!

Integration Testing in Ruby with RSpec's Story Automation Framework by David Chelimsky

Big thanks to David Chelimsky!

Thursday, March 27, 2008

Andrzej's Rails tips #9

link_to_remote and GET request

If you're working with link_to_remote and you are surprised with a message like the following:

Only get, put, and delete requests are allowed.

then you just need to know that link_to_remote uses POST request by default. All you need is to do is to add :method => :get to the method call.

Use schema.rb to create a new database

Keep the schema.rb file in your Subversion/Git/Mercurial repository and make sure it's up-to-date. It's very useful when you want to create a database without using migrations. You just call rake db:schema:load.

As the documentation states:

"Note that this schema.rb definition is the authoritative source for your database schema. If you need to create the application database on another system, you should be using db:schema:load, not running all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations you'll amass, the slower it'll run and the greater likelihood for issues)."

Monday, February 25, 2008

Andrzej's Rails tips #8

Resource controller and 'new' action

If you want to refer to the 'new' action using resource controller, you have to use the 'new_action' method.
BTW, if you want to do the same operation before both 'new' and 'edit' actions in a resource controller, you can do it like that:

[new_action, edit].each do |action|
action.before { @product_types = ProductType.find(:all) }
end

Migrations and removing defaults

If you have some default values set on some columns, then you can remove them using ':default => nil'.
Like that:

change_column :users, :name, :string, :default => nil

Tuesday, February 19, 2008

Andrzej's Rails tips #7

Dumping and loading data

Recently, I was in a need to dump data from a sqlite database and load it to a mysql database. There are many ways of doing such a task. One of them is using a plugin released by the Heroku people - YamlDb.

rake db:data:dump
rake db:data:load

You can install it with the following:

script/plugin install http://opensource.heroku.com/svn/rails_plugins/yaml_db

attachment-fu and capistrano

I use attachment-fu for one of my projects. One of the open questions here is how to deal with the files that our users uploaded to the server. I use capistrano 2, and ideally I prefer to have everything automated. The way you can use capistrano to deal with uploaded files is to have a directory which will be shared across different releases (the same as logs).

First, we need to tell attachment-fu that the upload directory is going to be public/uploads. Then, we need to tell Capistrano that public/uploads is going to be a shared directory. Both those things are nicely explained in the following article:

Working with attachment_fu

Monday, February 18, 2008

Andrzej's Rails tips #6

Redo a migration

There is a new rake task for redoing a migration:

rake db:migrate:redo

BTW, if you need to do it on a production server, then just append RAILS_ENV:

rake db:migrate:redo RAILS_ENV=production

and of course, if you want to list all rake tasks:

rake -T

Migrating to Rails 2.0 and mass-assignment warning

I was upgrading one application from Rails 1.2.3 to Rails 2.0. The application uses restful_authentication. One of the problems we have seen, was a warning in the log, saying something like 'mass-assignment no longer supported'. After investigation we found out that the problem was updating the user object. The User model was generated with restful_authentication, but later there were some application specific attributes added by a developer. Since restful_authentication generates a line like that:

attr_accessible :email

it means other attributes are not accessible. Adding the new attributes to this list solves the problem.

atttr_accessible :email, :birthdate

Monday, February 11, 2008

Andrzej's Rails tips #5

Two things today, both related to RSpec stories: webrat, and using regexps in RSpec stories.

Webrat with RSpec stories

What is Webrat? "Webrat lets you quickly write robust and thorough acceptance tests for a Ruby web application". It uses Hpricot under the hood and is very easy to understand just by looking at the code.

It took me only 30 minutes to turn most of my tests in one of my applications from a classic IntegrationTest-based RSpec story to Webrat. Here is one example:

When "he creates an order" do
visits '/'
clicks_link "New order"
fills_in "Nr", :with => 'abc/2008'
fills_in "Company", :with => 'ABC company'
selects 'New'
clicks_button 'Create'
end

Thanks to Ben, for his great article describing RSpec stories with Webrat.


RSpec, response.should have_text


Sometimes, all you need is just a check whether there is a certain message visible on a page. One way of doing that is with regexps. Here is an example step that checks for the message:


Then "he sees a $message" do |message|
response.should have_text(Regexp.new(message))
end

Thursday, February 7, 2008

Andrzej's Rails tips #4

will_paginate in other languages

The default labels for the will_paginate are 'Next' and 'previous'. It's trivial to change them like that:

<%= will_paginate :prev_label => 'Wstecz', :next_label => 'Dalej'%>

Rails plugins, Piston and URLs

I use piston for plugins management. It's a nice tool that allows you to call 'piston up' on any of the plugins, at any time to update. What it does worse than 'script/plugin' command is managing different repositories. With the 'plugin' command you can say:
script/plugin source URL
and it adds the URL to the list of known repositories.

What I use with Piston instead, is storing the urls as environment variables. Here is an excerpt of my ~/.bash_login file:

export ATTACHMENT_FU=http://svn.techno-weenie.net/projects/plugins/attachment_fu/
export RESOURCE_CONTROLLER=http://svn.jamesgolick.com/resource_controller/tags/stable/
export ATTRIBUTE_FU=http://svn.jamesgolick.com/attribute_fu/tags/stable
export RESTFUL_AUTHENTICATION=http://svn.techno-weenie.net/projects/plugins/restful_authentication/
export RSPEC=http://rspec.rubyforge.org/svn/tags/CURRENT/rspec
export RSPEC_ON_RAILS=http://rspec.rubyforge.org/svn/tags/CURRENT/rspec_on_rails

So I just type 'piston import $ATTA', press TAB, and the shell autocompletion does it for me.

Wednesday, February 6, 2008

Andrzej's Rails tips #3

Multi-model forms with attribute_fu

I've used attribute_fu in two of my projects now. In one of them I use it in three different places. It simplifies your code a lot, when you want to edit many models in one form. Strongly recommended!

attribute_fu

Gradual switch to resource_controller

resource_controller is a plugin that allows your controller's code to ... disappear :-)
A great thing with this plugin is that you can introduce it gradually. Just install it, and make one controller a resource controller. See how it goes, and then decide whether it was worth it. I did it like that, and now I'm switching to it wherever I can.

BTW, both attribute_fu and resource_controller are made by James Golick. Thanks, James!

restful_authentication with RSpec

I didn't know it before, but Bartosz Blimke discovered that, when you have a spec directory in your Rails app and you generate some resources using restful_authentication, then it will generate Rspec specs instead of unit tests. Cool! I'm not a big fan of code generation/scaffolding, but when they also provide good specs then it easies the pain.

Tuesday, February 5, 2008

Remote pair programming

This article describes my experience with remote pair programming, why I chose remote pair programming for one of the projects, how it works and what tools we use.

One of the projects I'm working on is a Rails application. We work on that together with my colleague, Marcin. There's nothing really innovative in this application. I've got enough experience with Rails and IT in general to know that it can be finished in a given time. The interesting bit is, that both me an Marcin live in different cities. Even better, both of us travel a lot and very often we are in different countries.

A distributed team is nothing new in today's IT world. One of the most common approaches to this problem is to divide the system and responsibilities across different people/teams/locations. It can work, I was involved in many such projects. The problem is that it requires a lot of trust, patience and really good skills to know how to split the project. In my opinion, it's a very risky approach, and it's easy to do it wrong. Chad Fowler describes it perfectly in his book: "My job went to India".

Traditional pair programming is a great practice. I know it's still controversial, and I agree that it doesn't have to work for all software developers and all projects, but I've seen it successful often enough to know that there are many benefits of pair programming. The benefits that I find important are:
  • Focus on the task
  • Sharing knowledge
  • Increasing trust
  • Fun
  • Speed of development
In our case, I think all of the factors are important. As always, time is a constraint, so it's important to focus on the most important tasks first. We have different skills - while I'm a little bit more experienced with Rails and TDD, Marcin is my guru when it comes to html/css. He worked on many different IT projects, so his general experience and intuition is very important for me. It means, we both gain a lot from this kind of cooperation. Fun of development is also an important factor here :-) From the remote pair programming sessions we've had so far I can say that I'm also very glad of our efficiency.

The tools we use are nothing new. We use vim + screen + skype. We both login to a development server as the same user. There is a screen session already open, so we just 'screen -x' to it, and now we see the same things at the same time. It's fun to watch, when Marcin is typing something, explaining it using skype, and I see it immediately in my vim. At any point, I can grab the keyboard and just start typing. Cool! I can't imagine working like that without a skype session, though. Using IM is just too slow.

I'm strongly recommending this kind of cooperation. I'm aware of the limitations here. The application must be possible to build on a Linux/Unix machine, but even if it can't, I can imagine some work-arounds to this problem. Different timezones might be an issue here. I don't think the whole project must be built like that. However, planning some remote pair programming sessions may increase the quality of your software.

I like this way of working so much, that I even consider doing it more often. Would anyone be interested in a Rails coaching session like that? All you need is skype and an SSH client.

The articles I found useful when preparing to remote pair programming:

http://blog.lathi.net/articles/2007/10/09/remote-pair-programming

http://mikeburnscoder.wordpress.com/2007/06/21/my-rails-development-environment-version-1-vim-and-screen/

Andrzej's Rails tips #2

Changing month names in date_select

Sometimes you may need to change the month names that appear in the date_select (or anywhere in your application). One way of doing it is the following (setting Polish months names):

class Date
MONTHNAMES = %w{Styczeń Luty Marzec Kwiecień
Maj Czerwiec Lipiec Sierpień
Wrzesień Październik Listopad Grudzień}
end
RSpec Stories, steps with parameters

There is an easy way of reusing steps in RSpec stories.
Let's say you had a following step:


Given a search for chess


which was implemented with a hardcoded value like that:

Given "a search for chess" do
@search_term = 'chess'
end


And now you'd like to add another, very similar scenario:

Given a search for bridge

You can reuse the previously implemented step by changing the implementation to:

Given "a search for $term" do |term|
@search_term = term
end


Thanks to that, you can now reuse this step in as many scenarios as you want.

You can find a nice explanation of RSpec stories on Edd Dumbill's blog.

Monday, February 4, 2008

Andrzej's Rails tips #1

has_many :through, :uniq=>true

It's quite common that when you need to use has_many :through, you actually want to see unique objects. Today, I had a need to display a list of producers based on an order.

In my design I have:

Order.has_many :line_items
LineItem.belongs_to :producer
Order.has_many :producers,
:through => :line_items

With this in place, if an order has two products, each of them belonging to the same producer, then order.producers lists the same producer twice.
Adding :uniq => true solves the problem:

Order.has_many :producers,
:through => :line_items,
:uniq => true


More information about it on John Susser's blog.

RESTful routes and pdf

If you have an action that responds to many formats (html, rss, pdf), you can still use a RESTful route with a 'formatted' prefix:

link_to "PDF format",
formatted_order_path(@order, :pdf)

Found on Railscast #78.

Thursday, January 17, 2008

RSpec User Story example

Today, just a quick example showing what kind of wonderful things you can do with the new RSpec and its support for executable User Stories, a feature I was dreaming about for years...

Specification as a user story:



Story: Creating an order

Scenario: admin user creates an order
Given an admin user
And some orders
And some customers

When he creates an order

Then a new order is created
And a new customer is created

Implementation of the user story



Given "an admin user" do
login_as_admin
end

Given "some orders" do
@orders_count = Order.count
end

Given "some customers" do
@customers_count = Customer.count
end

When "he creates an order" do
post '/orders/create',
"order"=>{"address_attributes"=> {"name"=>"Customer 1"}}
end

Then "a new order is created" do
Order.count.should == @orders_count + 1
end

Then "a new customer is created" do
Customer.count.should == @customers_count + 1
end

The output when the user story is run:



Running 1 scenarios

Story: Creating an order

Scenario: admin user creates an order

Given an admin user
And some orders
And some customers

When he creates an order

Then a new order is created
And a new customer is created

1 scenarios: 1 succeeded, 0 failed, 0 pending

Friday, January 4, 2008

A Guide to Deploying Rails Applications

This article describes the way I deploy my Rails applications.

Thanks to Ruby on Rails I can quickly help my customers. The speed of development with Rails and agile practices make my work really fast. The only time when I slow down a bit is when I create and deploy a new project. If I saved the time somehow I could spend this time solving business problems instead.
I decided to start with a documentation of my deployment/development process. Then, I automated and simplified all I could and had time to. The result is a semi-automatic process which helps me with creating applications. This process is specialised for my needs and it uses the tools that I found useful. It doesn't have to be good for your needs.
All the setup was focused on agility.
I love to check-in frequently and be able to see the changes immediately on the production server. That's why I use tools like Capistrano or Vlad.

A quick description of the tools I use:
  • Rails
    • I don't think I have to explain why :)
    • most of the time I use edge Rails
  • Mongrel
    • A standard way of deploying in the Rails world.
    • fast and stable
    • Easy to configure
  • nginx
    • an http server that is responsible for load balancing
    • easy to configure (even easier if you can read Russian blogs)
    • easy to handle many Rails applications.
  • Capistrano
    • A standard tool for easy deployment.
    • I use Vlad for some of my applications, both are good.
  • Piston
    • Easy plugins management
  • Subversion
  • mysql
Nginx configuration may be tricky when you work on a shared hosting but the main points stay the same also with Apache and other Http servers. Some of my applications are still on a shared machine (I recommend Webfaction), but most of them are on a dedicated machine.

Let's assume that you want to create and deploy a new Rails applications. The whole process can be described in 5 general steps.
  1. Create an SVN repository, create the app and import it to the repository.
  2. Use capistrano to make the remote work easier.
  3. Create the production database.
  4. Prepare mongrel_cluster to work with the new application.
  5. Configure nginx and bind the domain.

Ok, let's go into details.
  • Remote: Create a repository for the application.
    • svnadmin create /var/repos/app
      • I prefer to have a separate repo for each of my applications.
      • It's good to have one directory for all repositories
      • I use svn+ssh protocol, in my case it's easier to have just one source of users (Linux users)
  • Checkout the application to the /tmp/app directory
    • svn co $REMOTE_SVN/app /tmp/app
      • I recommend having environment variables for the svn paths, works nicely with shell autocompletion.
  • Use edge rails to create a rails app.
    • svn up ~/tmp/rails_edge
      • update it first
      • my rails_edge lives there, you can always create it with 'svn co $RAILS_DEV_TRUNK'
    • ruby ~/tmp/rails_edge/railities/bin/rails -dmysql /tmp/app/trunk
      • '-dmysql' because Rails has recently changed its default db to sqlite
    • svn ci -m "initial import"
  • Live on Rails edge
    • piston import $RAILS_DEV_TRUNK vendor/rails
    • svn ci -m "piston imported edge rails"
  • capify
    • capify .
    • It creates two files.
  • modify config/deploy.rb:

set :application, "app"
my_server = "12.34.56.7"
set :repository, "svn+ssh://#{my_server}/var/repos/app/trunk"
set :deploy_to, "/var/www/#{application}"
role :app, my_server
role :web, my_server
role :db, my_server, :primary => true

  • Commit.
    • svn ci -m "capistrano setup"
  • cap deploy:setup
    • it creates all the directories on the remote server
  • cap deploy:cold
    • it checkouts the code, creates a 'current' symlink
    • logs are in the 'shared' directory
    • it fails on db:migrate task which is fine for now.
  • Create the production database
    • Remote: Go to /var/www/app/current
    • Remote: rake RAILS_ENV=production db:create
  • Local: cap deploy:cold
    • Again.
    • It should pass the db:migrate now
    • But it fails on missing script/spin file
  • Create script/spin with the following content:
    • mongrel_cluster_ctl restart
    • Yes, you need mongrel_cluster on the remote server
    • Add this file to svn and commit
  • cap deploy:cold
    • This time it worked!
    • But... we didn't setup mongrel_cluster to restart our app.
  • Mongrel_cluster
    • Create config/mongrel_cluster.yml
---
cwd: /var/www/app/current
log_file: log/mongrel.log
port: 5000
environment: production
group: www-data
user: your-username
address: 127.0.0.1
pid_file: tmp/pids/mongrel.pid
servers: 3
  • Commit this file
  • cap deploy:cold
    • still doesn't work
    • Mongrel_cluster doesn't know that it should use this file
  • Remote: Go to /etc/mongrel_cluster/
  • Make a symbolic link to the yml file
    • sudo ln -s /var/www/app/current/config/mongrel_cluster.yml app.yml
  • cap deploy:cold
    • And it works!
    • you should now be able to connect to localhost:5000 from the remote server.
  • Now we will bind the domain to this mongrel_cluster.
    • We need to configure nginx.
    • I have setup nginx so that for every application I create a single file that is automatically loaded on nginx startup.
    • The file just needs to live in the /etc/nginx/vhosts/ directory.
    • It works because I have the following line in my /etc/nginx/nginx.conf file:
      • include /etc/nginx/vhosts/*.conf;
  • Create /etc/nginx/vhosts/app.conf

upstream app {
server 127.0.0.1:5000;
server 127.0.0.1:5001;
server 127.0.0.1:5002;
}
server {
listen 80;
client_max_body_size 50M;
server_name app.com www.app.com;
root /var/www/app/current/public;
access_log /var/www/app/current/log/nginx.log;

if (-f $document_root/system/maintenance.html) {
rewrite ^(.*)$ /system/maintenance.html last;
break;
}

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect false;
proxy_max_temp_file_size 0;
if (-f $request_filename) {
break;
}
if (-f $request_filename/index.html) {
rewrite (.*) $1/index.html break;
}
if (-f $request_filename.html) {
rewrite (.*) $1.html break;
}
if (!-f $request_filename) {
proxy_pass http://app;
break;
}
}

error_page 500 502 503 504 /500.html;
location = /500.html {
root /var/www/app/current/public;
}
}

  • Restart nginx
    • sudo /etc/init.d/nginx stop
    • sudo /etc/init.d/nginx start
  • That's it :)
  • From now on you just write the code, commit and call capistrano to deploy.

I hope you found this guide useful. There is still a lot to improve and a lot of duplicates (like defining mongrel ports). Let me know if you think that I could do something in a better way.

Here is a list of articles that I found useful when I was experimenting with Rails deployment.

Installing and Configuring Nginx and Mongrel for Rails

Install Ruby Rails on Gutsy Gibbon (Nginx Version)


Hosting Rails apps using nginx and Mongrel Cluster

A clean slate, Edge Rails recipe

Thursday, October 18, 2007

Rails: How to unit test the formatting of model fields in the views.

In our Rails applications we often use Textile/RedCloth or some other libraries to have things like Post's content nicely formatted in the view. I use acts_as_textiled. This plugin simplifies the code a lot. All that is needed in order to make a certain field 'textilized' is this line:

class Post < ActiveRecord::Base
acts_as_textiled :content
end

The question that I want to focus on here is:

How to unit test that a certain field will be nicely formatted in the views?

lolcat - this not so hard
more funny pictures

As Jay Fields has already explained (he focused on testing validations in his article) there are two approaches:
The first one is based on listing all the cases that are important, like whether paragraph tags are correctly inserted or whether bold tags are correct. It may be sometimes useful but for me it's a code smell that for testing a single line in your codebase you need so many test lines of code. Additionally, the acts_as_textiled plugin is nicely tested/specified so why duplicate the code? I'd rather test some of those cases in my Selenium tests or functional tests and choose the second approach:

def test_content_is_nicely_formatted
Post.expects(:acts_as_textiled).with(:content)
load "#{RAILS_ROOT}/app/models/post.rb"
end

I'm using mocha here for setting an expectation that a class method called acts_as_textiled will be called (passing the field name argument) during the load time of the class file. This solution is not ideal. I don't like hardcoding the path here but I still find it simpler that the first approach.
This way testing of things like validations and acts_as_* calls becomes much simpler.

Thursday, June 28, 2007

RSpec for acts_as_taggable

Requirements:

An empty post
- should be valid
- should have no tags
- should allow setting tags

A post with tags
- should show tags
- should be listed as tagged


Implementation:

class Post < ActiveRecord::Base
acts_as_taggable
end


RSpec:

describe "An empty post" do
before(:each) do
@post = Post.new
end

it "should be valid" do
@post.should be_valid
end

it "should have no tags" do
@post.tags.should be_empty
end

it "should allow setting tags" do
@post.tag_list = "tdd, rails"
@post.save
@post.tags.should_not be_empty
end
end

describe "A post with tags" do
before(:each) do
@post = Post.new(:tag_list=>"tdd, python, rails")
@post.save
end

it "should show tags" do
@post.tag_list.names.should == ["tdd", "python", "rails"]
end

it "should be listed as tagged" do
Post.find_tagged_with('tdd').should == [@post]
end
end


More details:
http://agilewebdevelopment.com/plugins/acts_as_taggable_on_steroids

Thursday, June 14, 2007

Testing Rails controllers with mock objects

One of the steps in my 15 TDD steps to create a Rails application article was using the mocking technique.
Mock objects were used in a test for a controller method. From the feedback I received I noticed that this technique is not very popular and requires a more detailed explanation. This article describes why you might want to use mock objects for testing your controllers and how it simplifies the testing setup and code. The mocking library I use in this article is Mocha.

Rails controllers

Before I go into details about testing Rails controllers, let's clarify what is the responsibility of a Rails controller.

Ideally, a controller method should only call one method on a model class. Sometimes the method on the model that we call is some kind of finder like Person.find(:all). The result of the method should then be passed to the view.
A Rails controller should be a thin layer between your model classes and the view part.

A sample implementation looks like that:

def learn
@word = Word.random
end

As we see, we call the 'random method on the Word class which represents some model part of our application. Then we pass the result to the view via @word.

Testing Rails controllers

Being aware of that, we know what we want to test:
A learn method should:

  1. call the Word.random method

  2. pass the result to the view


Problems with traditional approach to testing Rails controllers
Now, what if we simply construct the test like this:

def test_learn_passes_a_random_word
get 'learn'
assert_kind_of Word, assigns('word')
end

The first problem here is that it calls the Word.random method, thus accesses the (test) database. It means we need to have some fixtures loaded so that the random method can retrieve some word from it.
The second problem is that we only test the type (kind_of) of the object assigned to @word. It means we don't fully test the method. We could imagine an implementation of the learn method that passes the tests but does the wrong thing (by assigning any other object to @word). Not ideal.

Testing with mock objects

Fortunately, we can use mock objects here and solve both problems.
We will replace the Word.random method with a method that always returns an object that we prepared before. Another advantage is that:

We don't connect to the database from our unit tests which is A Good Thing.


The object that we prepare before can then be used to assert whether the one passed to the view was the same as the result of calling Word.random method.

random_word = Word.new
Word.expects(:random).returns(random_word)

As we see, we prepare a random_word object, which is simply a new instance of a Word class. The next line does two things:

  • expects(:random) - sets our expectation that the random method will be called

  • returns(random_word) - replaces the real implementation of the 'random' method with an implementation that always returns the random_word object.


Btw, we didn't have to create a new instance of Word here. We could have used Object() there and that would work fine. The crucial part is that we compare the objects we return from Word.random with the one assigned to @word.

What is an expectation?

An expectation is almost like an assertion. The difference is that we set it before the actual call to the method that we test. The test will fail if the expectation isn't met.

We now extend the test with a call to the 'learn' method that we test:

random_word = Word.new
Word.expects(:random).returns(random_word)
get 'learn'

This test should now pass with the following implementation:

def learn
@word = Word.random
end


We also want to test that the object was assigned to @word, so we need an additional line with assertion:

def test_learn_passes_a_random_word
random_word = Word.new
Word.expects(:random).returns(random_word)
get 'learn'
assert_equal random_word, assigns('word')
end


The assertion tests the equality of the random_word object and the @word (we access it using assigns('word')). It's possible only thanks to the fact that we replaced the real Word.random method and have a reference to the result of this method.
Benefits

  • not connecting to a database
  • no need for preparing fixtures
  • faster tests
  • controller tests are less fragile for model changes

Drawbacks

  • some developers may find the tests less readable
  • if you rename the model method, the controller tests will still pass
  • increased need for some integration tests - Selenium or standard Rails integration tests


Conclusion

I hope that this article sheds more light on what are mock objects. I strongly recommend trying to add some tests like that to your test suite. If you want to read more about mocking you should first go to the classic Martin Fowler's article on Mock Objects. After that go to the Jay Field's blog. You can also find some interesting examples on the Mocha website.

Monday, May 28, 2007

Selenium on Rails in 5 minutes

Introduction
Selenium is a very good framework for testing web applications. It's an ideal tool to use when you want to test your Rails apps from a user perspective. There is a Selenium on Rails plugin that simplifies creating and running Selenium tests.

Step 1. Create a Rails app and install the Selenium on Rails plugin

rails selenium_rocks
cd selenium_rocks
script/plugin install http://svn.openqa.org/svn/selenium-on-rails/selenium-on-rails

(it may take a while)

Step 2. Configuration
Create vendor/plugins/selenium-on-rails/config.yml file and paste the following:

environments:
- test
browsers:
safari: '/Applications/Safari.app/Contents/MacOS/Safari'
#firefox: 'c:\Program Files\Mozilla Firefox\firefox.exe'
#ie: 'c:\Program Files\Internet Explorer\iexplore.exe'

Change the browser path to point to your browser.

Step 3. Create a Selenium test

script/generate selenium welcome_page_test

Edit test/selenium/welcome_page_test.sel. Replace the existing content with the following:

setup
open '/'
assert_title 'Ruby on Rails: Welcome aboard'

Step 4. Start the server in a test environment
run in a new terminal:

script/server -e test

Step 5. Run the Selenium test

rake test:acceptance

You should see the following output:

1 tests passed, 0 tests failed

Congratulations!

More information:
Selenium On Rails website
Full-stack Web App Testing with Selenium and Rails (RailsConf 2007)