In my previous article we took a Behavior Driven Development approach to testing our data layer, in which our models were tested using RSpec. In this article I will showcase how RSpec can be used for controller testing. If you are new with RSpec, I will not go into detail with basic RSpec syntax such as should and it, please read my previous article, TDD, BDD and Using RSpec which goes over the basics to get you started.
Before we dive into controller testing, let's quickly create our app that will help birdkeepers find information on birds. Run the following commands:
rails birdkeeper -d mysql cd birdkeeper script/generate scaffold Bird title:string species_id:integer notes:text
Create birdkeeper_development and birdkeeper_test databases, add your database credentials to config/database.yml and migrate:
rake db:migrate rake db:migrate RAILS_ENV=test
Then add the RSpec plugins (through git), gem (if not installed) and generate the spec directories:
sudo gem install rspec script/plugin install git://github.com/dchelimsky/rspec.git script/plugin install git://github.com/dchelimsky/rspec-rails.git script/generate rspec
Open up app/controllers/birds_controller.rb, it should contain the 7 CRUD actions created from the Rails scaffold generator. If there are no actions, your version of Rails may need to be updated. Create birds_controller_spec.rb inside of spec/controllers. Add the following code:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe BirdsController do end
The first line loads in the spec_helper file which will contain common code that can be shared between specs. Next we have a describe block which helps keep our tests organized. This one will contain tests relating to the BirdsController, which will hold all our tests. It will also contain inner describe blocks for further organization. Let's test the update method, since in addition to performing a find call like most CRUD actions, it also updates the object. The update method we will be using is as follows:
# PUT /birds/1 # PUT /birds/1.xml def update @bird = Bird.find(params[:id]) respond_to do |format| if @bird.update_attributes(params[:bird]) flash[:notice] = 'Bird was successfully updated.' format.html { redirect_to(@bird) } format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @bird.errors, :status => :unprocessable_entity } end end end
Go back to the BirdsController spec and add the following inside of the BirdsController describe block:
# UPDATE describe "PUT birds/:id" do describe "with valid params" do end describe "with invalid params" do end end
Above we have added 3 describe blocks. One wrapper describe block will contain all tests relating to the update method. Inside there are two describe blocks, one with tests if valid params are given and the other if invalid params are given. Let's start with valid parameters. Before writing the actual tests, we need to set expectations. This is done through mocking and stubbing within a before block. Add the following inside the "with valid params" describe block:
before(:each) do @bird = mock_model(Bird) Bird.stub!(:find).with("1").and_return(@bird) end
The before block will run before each test. This will DRY up our tests since we won't have to rewrite the same mocks and stubs for each test. Mocks and stubs allow us to test the controller functionality without relying on ActiveRecord. With mock_model we are imitating a Bird object. Stubs are used to fake method calls, we don't need to know the details of the actual method. We just know that the Bird class will receive a find call with a argument of "1" and it should successfully return a @bird object, which will be our mock. Then later in the same method, the @bird mock will receive an update_attributes call, so we also need to stub this call out. We can stub it out as follows:
@bird.stub!(:update_attributes).and_return(true)
But there is another way to accomplish this much more DRYly. Our mock model accepts a optional hash of method calls and their return value. We can modify our @bird mock object into:
@bird = mock_model(Bird, :update_attributes => true)
With the before block set up we can start writing tests. Tests are contained in it blocks and takes a string argument explaining its contents. Let's first test the find call, which is the first action to happen after update is called.
it "should find bird and return object" do Bird.should_receive(:find).with("1").and_return(@bird) put :update, :id => "1", :bird => {} end
Here we are testing if the Bird class received a find call, with the should_receive syntax. The rest is very similar to the stub method since we are checking if it received "1" as an argument and returned a @bird object.
After a Bird object is found, its attributes are updated. Testing this call is very similar to the find call:
it "should update the bird object's attributes" do @bird.should_receive(:update_attributes).and_return(true) put :update, :id => "1", :bird => {} end
Next we make sure a flash notice is set:
it "should have a flash notice" do put :update, :id => "1", :bird => {} flash[:notice].should_not be_blank end
If the controller can have one of many flash notices, we can also be more specific:
it "should have a successful flash notice" do put :update, :id => "1", :bird => {} flash[:notice].should eql 'Bird was successfully updated.' end
After the flash notice is set, the user should get redirected to the bird's show page. We can test the redirect by accessing the response object as so:
it "should redirect to the bird's show page" do put :update, :id => "1", :bird => {} response.should redirect_to(bird_url(@bird)) end
As for testing if there was invalid data, we would do this:
before(:each) do @bird = mock_model(Bird, :update_attributes => false) Bird.stub!(:find).with("1").and_return(@bird) end it "should find bird and return object" do Bird.should_receive(:find).with("1").and_return(@bird) put :update, :id => "1", :bird => {} end it "should update the bird object's attributes" do @bird.should_receive(:update_attributes).and_return(false) put :update, :id => "1", :bird => {} end it "should render the edit form" do put :update, :id => "1", :bird => {} response.should render_template('edit') end
Most is similar to the valid data version, but there are a few differences. We are stubbing the object's update_attributes call to return false, this will cause the conditional to take the else route. Since there are errors, it needs to render the edit page. We test this by doing a response.should render_template('edit').
This is how the complete Bird Controller spec looks:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe BirdsController do # UPDATE describe "PUT birds/:id" do describe "with valid params" do before(:each) do @bird = mock_model(Bird, :update_attributes => true) Bird.stub!(:find).with("1").and_return(@bird) end it "should find bird and return object" do Bird.should_receive(:find).with("1").return(@bird) end it "should update the bird object's attributes" do @bird.should_receive(:update_attributes).and_return(true) end it "should redirect to the bird's show page" do response.should redirect_to(bird_url(@bird)) end end describe "with invalid params" do before(:each) do @bird = mock_model(Bird, :update_attributes => false) Bird.stub!(:find).with("1").and_return(@bird) end it "should find bird and return object" do Bird.should_receive(:find).with("1").return(@bird) end it "should update the bird object's attributes" do @bird.should_receive(:update_attributes).and_return(false) end it "should render the edit form" do response.should render_template('edit') end it "should have a flash notice" do flash[:notice].should_not be_blank end end end endRun this spec from the root of your application with the following command, all specs will pass:
ruby spec/controllers/birds_controller_spec.rb
Using the information learned here can be applied to writing tests for the other 6 CRUD actions. Since these actions were generated through the Rails scaffold generator, they should work and tests may not be needed. On the other hand, changes in functionality may cause new bugs to pop up. It is recommended to write tests and cover as much possible. It may become tedious to write tests for basic CRUD actions for each generated controller, luckily RSpec has a scaffold generator that will generate the same files complete with RSpec tests. The RSpec version of the scaffold can be used as follows:
script/generate rspec_scaffold Bird title:string species_id:integer notes:text
As a bonus, let's say we created a Species scaffold. A bird will belong to Species and we have the Birds controller nested within the Species controller. On the Species show page, we would run the following find methods for a list of birds that belong to that Species. Just how should the following be mock and stubbed?
@species = Species.find("1") @birds_in_species = @species.birds.find(:all)
Take a few minutes to think about it. It is a bit more complicated, but like any complicated matter, can be simplified by breaking it down. I start off by mocking all objects involved, @species and @birds_in_species are a given. But we also can't forget the birds that are going to be returned from @species.birds, that needs to be mocked as well.
@birds_in_species = mock_model(Bird) @species = mock_model(Species) @birds = mock_model(Bird)
As for stubbing out the method calls. There are three in total, @species.birds.find counts as two.
Species.stub!(:find).with("1").and_return(@species) @species.stub!(:birds).and_return(@birds) @birds.stub!(:find).and_return(@birds_in_species)
Then in our tests, we would do the following:
Species.should_receive(:find).with("1").and_return(@species) @species.birds.should_receive(:find).and_return(@birds_in_species)
I hope you have found this article helpful on testing controllers with RSpec. If you have checked out the generated specs through the rspec_scaffold generator, there are less tests for the update method. I prefer to have many smaller tests with each testing a small portion of the controller, that way when a single line is changed, the error from the test will be more helpful since it is more specific. I have found out about this approach from Mike Mangino, though ultimately how your specs are organized is a matter of personal preference.
What is BDD and TDD?
When it comes to testing our specs and code, Rails developer have two mainstream choices. We can use Test::Unit, which is supplied by the Ruby Standard Library and is immediately supported by Rails, or we can use the RSpec gems and plugins. Although either one can get the job done, they vary significantly in how they approach testing.
Test::Unit supports a Test Driven Development (TDD) approach, while RSpec implies a Behavior Driven Development (BDD) approach. In TDD, we are usually writing one test per function, leading to many small tests. In TDD, since there is one test per function, we end up writing tests at a core level that end up following the same structure as our code. With a BDD approach, we are using specs as a guide for testing. We are telling a story by describing how a set of functionality should behave, and focus less on the return value of a single method. Think of BDD as a humanized TDD; it is TDD written to use a more English like vocabulary for expressing functionality. BDD makes it easier for other developers to read and understand the code, becoming an additional form of documentation. BDD changes the way we think about testing, since it is more focused on implementing high level specs.
Turning our attention back to Rails, we have access to both Test::Unit and RSpec testing frameworks. Test::Unit is more focused on TDD, while RSpec is more focused on BDD. It is possible to use a BDD approach with Test::Unit, though doing so wouldn't be as clear and concise as using RSpec. Each has their own pros and cons; for example, future updates may break our RSpec tests, causing developers to go back and update their tests into a working state. Test Unit tests won't be affected since it is part of the Ruby Standard Library and built into Rails. Which ever framework you do choose to use will be sufficient for testing, so it's more a question of your preferred testing ideology.
At Killswitch, we have chosen to use the BDD testing paradigm with RSpec. The following is a brief tutorial in getting up and running with RSpec and adopting the BDD approach.
Getting Started:
1. Create a new Rails app:
rails rspec_demo -d mysql
2. Set up your test database and development databases.
3. Install the RSpec Gem:
sudo gem install rspec
4. Install Plugins (git needed):
script/plugin install git://github.com/dchelimsky/rspec.git script/plugin install git://github.com/dchelimsky/rspec-rails.git
5. Generate RSpec directories in root of application:
script/generate rspec
Using RSpec
First, let's create some models. RSpec gives us a new rspec_model command; it will create the model similar to using the Rails model generator, except it will create RSpec files instead of Test::Unit files:
script/generate rspec_model salesman first_name:string last_name:string total_products_sold:integer script/generate rspec_model category title:string script/generate rspec_model product title:string quantity:integer category_id:integer script/generate rspec_model saleman_product saleman_id:integer product_id:integer amount_sold:integer
Before migrating, add 0 as a default value in the migrations for the following columns: total_products_sold in salesman, quantity in product, amount_sold in salesman_products.
Run our migrations with:
rake db:migrate rake db:migrate RAILS_ENV=test
Next, we need to set up relationships and validations in our models:
# app/models/salesman.rb class Salesman < ActiveRecord::Base has_many :salesman_products has_many :products, :through => :salesman_products validates_presence_of :first_name validates_presence_of :last_name end # app/models/product.rb class Product < ActiveRecord::Base has_many :salesman_products has_many :salesmen, :through => :salesman_products belongs_to :category end # app/models/category.rb class Category < ActiveRecord::Base has_many :products validates_presence_of :title end # app/models/salesman_product.rb class SalesmanProduct < ActiveRecord::Base belongs_to :salesman belongs_to :product end
Open up salesman_spec.rb in spec/models. It should contain the following:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe Salesman do before(:each) do @valid_attributes = { :first_name => "value for first_name", :last_name => "value for last_name", :total_products_sold => "1" } end it "should create a new instance given valid attributes" do Salesman.create!(@valid_attributes) end end
The first line includes the spec_helper which is inside of the spec folder. This file will hold helper methods that all specs will use, I will not be going into detail on creating spec helpers, if you would like to learn more about spec helpers check out the official rspec site.
After the spec helper we have a describe block which will describe a given situation. One can put all their tests in one description block, but I usually like to create multiple description blocks that contains logic for a new record, existing records and relationships. Inside the description block there is a before block that sets up each test with needed data.
There is also an it block inside of the describe block, which is an individual test. One describe block can contain many it blocks. Each it takes a string argument that should describe the test logic it contains. With it being Salesman, the example test would read as "Salesman should create a new instance given valid attributes"
Before moving on, lets make it clear the type of tests this describe block will contain. Each describe block can take in a optional string argument. Lets test a new Salesman object, add "that is new," as the second argument in the describe block. Now the test would read as "Salesman that is new, should create a new instance given valid attributes"
describe Salesman, "that is new," do before(:each) do @valid_attributes = { :first_name => "value for first_name", :last_name => "value for last_name", :total_products_sold => "1" } end it "should create a new instance given valid attributes" do Salesman.create!(@valid_attributes) end end
Try running the spec with:
ruby spec/models/user_spec.rb
You should see:
1 examples, 0 failures
RSpec does not print successful tests, only ones that failed. If there was a error, for example a NoMethodError, RSpec would tell us like so:
NoMethodError in 'Salesman that is new, should create a new instance given valid attributes'
Now since we are creating a new salesman, lets remove the total_products_sold set from the hash. Total products sold is not required, so let's write a new test that will check if a new salesman starts with the value of 0 for total_products_sold. While we're at it, lets change the "value for ...." default strings in the before block to have more realistic data. Currently our spec should look similar to the following:
describe Salesman, "that is new" do before(:each) do @valid_attributes = { :first_name => "Tony", :last_name => "Little" } end it "should create a new instance given valid attributes" do Salesman.create!(@valid_attributes) end it "should create a new salesman with zero total products sold" do end end
Usually in Ruby when we want to make sure an attribute equals a certain value we usually use:
salesman.total_products_sold == 0
Using such syntax would make our tests look similar to the actual code. However, since RSpec has its own syntax similar to English, why not use the RSpec equivalent for more readable tests:
salesman.total_products_sold.should eql(0)
Lets test for zero:
it "should create a new salesman with zero total products sold" do salesman = Salesman.create!(@valid_attributes) salesman.total_products_sold.should eql(0) end
Now when we run our specs, both tests should be a success.
So far we have made sure when valid data is used, our tests will pass. What if we wanted to test for errors? RSpec has syntax for testing errors. In our code we would check if an object is valid with the following:
salesman.valid?
The RSpec equivalent:
salesman.should be_valid
It's pretty straight forward, we are stating the salesman should be valid. We can also test to make sure it is not valid with salesman.should_not be_valid. Lets try it out by adding the following test:
it "should not be valid without a first name" do @valid_attributes[:first_name] = nil salesman = Salesman.new(@valid_attributes) salesman.should_not be_valid end
In the above example we first changed the value of first_name to nil, and then we pass those attributes into a new Salesman object. Then when it reaches salesman.should_not be_valid, it will run through the validations, check to see if it was valid, if not, it will return true and our test will pass!
We can be more detailed and thorough in the previous example by checking to make sure the first_name attribute returns a error. To do this, we simply add the following to the end of the test:
salesman.should have(1).errors_on(:first_name)
We did a few tests for a brand new Salesman, so now let's do some model testing with existing data. Before starting, we will need some data that will be used throughout all our tests. We can use fixtures to create test objects which will add data to our test database. The test database will be reset before each test so changes to data from the previous test won't break the next test. Open up salesmen.yml inside of spec/fixtures and enter the following data:
mays: first_name: Billie last_name: Mays total_products_sold: 356 norris: first_name: Chuck last_name: Norris total_products_sold: 155 simmons: first_name: Richard last_name: Simmons total_products_sold: 186
So far we have created a describe block for a new salesman, next we will create another describe block for existing salesmen. I always separate existing from new for not only organizational purposes, but also for speed purposes. If we were to have many tests in a single describe block and load unnecessary fixtures for every test in a spec, it would lead to really slow tests since the database is reset every time with fixture data. So let's add a new describe block that will contain only the tests requiring fixtures. I have added comments in the code:
describe Salesman, "that exists" do # loads spec/fixtures/salemen.yml fixtures :salesmen before(:each) do # Using fixtures, not needed end it "should have a collection of salesmen" do # !Salesman.find(:all).empty? Salesman.find(:all).should_not be_empty end it "should have three records" do # Salesman.count == 3 Salesman.should have(3).records end it "should find an existing salesman" do salesman = Salesman.find(salesmen(:mays).id) salesman.should eql(salesmen(:mays)) end end
For the last test we are using salesmen(:mays), this will grab the attributes with the title 'mays' from the salesmen.yml file. Since mays has an id of 1, it will tell ActiveRecord to grab the salesman with the id of 1. Next we check to make sure that the salesman object has the same attributes found in the .yml file.
Let's quickly add one more describe block that will test relationships between salesmen and products. Once again we will need fixtures, so open up products.yml and saleman_products.yml and add the following:
# products.yml
oxiclean:
id: 1
title: OxiClean
quantity: 35
category_id: 1 # Cleaning
orangeglow:
id: 2
title: OrangeGlow
quantity: 42
category_id: 1
discosweat:
id: 3
title: Disco Sweat
quantity: 22
category_id: 2 # Video
platinumsweat:
id: 4
title: Platinum Sweat
quantity: 34
category_id: 2
totalgym:
id: 5
title: Total Gym Workout Equipment
quantity: 11
category_id: 3 # Exercise Equipment
handyswitch:
id: 6
title: Handy Switch
quantity: 23
category_id: 4 # Home
fixit:
id: 7
title: Fix It
quantity: 14
category_id: 5
liquid_diamond:
id: 8
title: Liquid Diamond
quantity: 7
category_id: 5 # Auto
# saleman_products.yml
one:
saleman_id: 1
product_id: 1
amount_sold: 75
two:
saleman_id: 1
product_id: 2
amount_sold: 89
three:
saleman_id: 2
product_id: 3
amount_sold: 87
four:
saleman_id: 2
product_id: 4
amount_sold: 99
five:
saleman_id: 3
product_id: 5
amount_sold: 45
six:
saleman_id: 1
product_id: 6
amount_sold: 81
seven:
saleman_id: 1
product_id: 7
amount_sold: 76
eight:
saleman_id: 1
product_id: 8
amount_sold: 45
Finally add in a new describe block that will load only fixtures needed for testing salesmen and products relationships.
describe Salesman, "with Products" do # Will load 3 fixtures need for testing Salesman Product relationships fixtures :salesmen, :salesman_products, :products before(:each) do # Using fixtures end it "should have products" do salesman = Salesman.find(salesmen(:mays).id) salesman.products.should_not be_empty end it "should have five products" do salesman = Salesman.find(salesmen(:mays).id) salesman.products.should have(5).records # should include will check if product belongs to salesman salesman.products.should include(products(:oxiclean)) salesman.products.should include(products(:orange_glow)) salesman.products.should include(products(:handy_switch)) salesman.products.should include(products(:fix_it)) salesman.products.should include(products(:liquid_diamond)) end it "should not have products from other salesmen" do salesman = Salesman.find(salesmen(:norris).id) # should_not include is the opposite of should include # we are making sure this salesman, doesn't have products that # other salesmen are selling salesman.products.should_not include(products(:disco_sweat)) salesman.products.should_not include(products(:platinum_sweat)) salesman.products.should_not include(products(:orange_glow)) salesman.products.should_not include(products(:liquid_diamond)) end end
Now with your feet soaked into the world of BDD, it is time for you to start writing tests for your own applications. If you would like to continue building over the example above, more validations can be tested and specs can to be written for the other models as well. Be sure to check out the official RSpec site for more info.
When mapping the flow of an application, we usually have various states for a single object. These states are then joined with lines showing how an object can transition from one state to another. Each state will be unique; it may be just simply have a status flag, or it could open up new functionality within the app. For example, an e-commerce application may have a system for package tracking. When customer pays for a product, that product will have a status of 'paid'. Based on the paid status, the seller will be notified that the product needs to be shipped and the customer will gain the ability to write a review.
Traditionally, we would have to create methods that dealt with switching and checking states. Thankfully that has all changed with the wonderful ActsAsStateMachine plugin. What I like best about ActsAsStateMachine is that it has its own methods in handling states and event transitions that won't get lost with other methods within the model. I will go more into detail soon, but first we need to create a quick application to showcase the awesomeness of ActsAsStateMachine!
Let's create a baseball application that will display scores of current games. Each game will be in the database before it starts, allowing users to view if tickets are available. When the game is live, ticket information will not be needed and a scoreboard with all the innings will be displayed instead. When the game is over, only the final score will appear with information for the next game.
To get started, first install the plugin:
ruby script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk/
Then within our Game model, add the following:
acts_as_state_machine, :column => :game_state, :initial => :pending
Here we are simply telling the Game model to act as a finite state machine. We are setting the column that will hold the Game's state as game_state. By default, ActsAsStateMachine will use the state column. This can cause problems since a state column in most applications will hold location information. So by specifying a column name, we can avoid column problems. The last portion will set the initial state of a newly created Game object to pending. It is similar to creating a migration and setting the column to a default value, but if that default value ever needs to change, a new migration would have to be created. By setting a initial value, we can easily change just that one line.
Now lets define all the various states for a game. Based on the app's requirements, a game will have three states: Pending, Live and Final. Defining states is as easy as adding:
state :pending state :live state :final
Our Game model now has states and with each state definition new methods are created. These methods may come in handy for state-specific validations or can be used in conditionals within the views that will load certain partials. For example, instead of doing @game.state == "live", we can use @game.live?.
With all the states for Game defined, it is time to add events that will transition the game from one state to another. ActsAsStateMachine has it's own methods for creating events, which is basically a loop. We start by defining the event loop, giving the event a name and then specifying transitions within.
event :start_game do transitions :from => :pending, :to => :live end event :end_game do transitions :from => :live, :to => :final end
With these events we can now transition a game object's state by using @game.start_game! and @game.end_game!. Keep in mind each event can hold many transitions, we can combine the two event methods into one as long as the :from portion is different. It wouldn't make much sense in the context of our example, however, because it is linear.
We have had the app up for a few weeks now and baseball fans are really digging the slick up-to-the-minute scores that we update through AJAX calls. Eventually we will run into our first rain delay, which will lead to problems. Fans will start thinking the app has stopped updating and our servers will keep running expensive AJAX updates over a game that has turned idle. It is time to introduce the rain delay state and event, making our simple app less linear.
state :rain_delay event :rainout do transitions :from => :live, :to => :rain_delay end
With a rain delay, a choice will be made by the umpires. If enough innings have been played, the game can end with the current score being final. Otherwise the game can be canceled and be rescheduled at a later date. Let's add the new canceled state and transitions events from rain_delay. Lets also add a new transition into the end_game event for rain_delay games that have played enough innings to be called final.
state :canceled event :end_game do transitions :from => :live, :to => :final transitions :from => :rain_delay, :to => :final end event :cancel_game do transitions :from => :rain_delay, :to => :canceled end
Let's say if a game is canceled it is still in our database for historical purposes and a new game will be created with some data from the canceled game. Let's also say that the reschedule_game method will create a new game from a canceled game. ActsAsStateMachine lets us attach callbacks like state events. Basically we want to run the reschedule_game method whenever a game's state changes to canceled. To accomplish this, we simply add :enter => methodname after defining the state as so:
state :canceled, :enter => :reschedule_game
Now whenever a game's state switches to canceled, a copy will automatically be made through the reschedule_game method. Besides enter, ActsAsStateMachine also has after and exit options. After would run after the state switch has been made and exit will be executed when the object is transitioning away from the state.
ActsAsStateMachine comes in handy when a object's status goes through many changes. I hope I have shown you how easy it is to define states, create transitions and implement ActsAsStateMachine into a model. Here is what our final code looks like:
acts_as_state_machine, :column => :game_state, :initial => :pending # States state :pending state :live state :final state :rain_delay state :canceled, :enter => :reschedule_game # Transition Events event :start_game do transitions :from => :pending, :to => :live end event :end_game do transitions :from => :live, :to => :final transitions :from => :rain_delay, :to => :final end event :rainout do transitions :from => :live, :to => :rain_delay end event :cancel_game do transitions :from => :rain_delay, :to => :canceled end

