In the last entry in my Rails 3 series we looked at hooking up a Controller and View so that we could render a page in the browser. By doing this we technically had a working Rails app, but we were missing one piece of the MVC puzzle, the model! In order to really connect the dots, we need all three pieces in place.
But before we go there, I want to hook up one more piece in our Rails 3 application…RSpec. You have probably heard in the past just how crazy Rails developers are about testing. Well, everything you have heard is true. Not only is testing a very good thing, but testing in languages that are as dynamic and flexible as Ruby are absolutely critical. While I am certainly not a TDD wizard by any measure, I do enjoy a nicely written, tested codebase.
Because in Rails we sometimes like to generate code, I would like to go ahead and get RSpec pulled in and wired up so that when we start to create our model and controllers, we get some example test code along with it.
Setting Up RSpec
The first step is to add the RSpec-Rails gem to our test and development groups in our Gemfile. We don’t want to just add "rspec-rails" to our Gemfile like this:
Because then we might get our version of RSpec switched out on us when we aren’t expecting. We want to specify a version so that we can update our dependencies in a predictable manner. So first we have to figure out what the latest version of the ‘rspec-rails’ gem is. This can be accomplished by running:
gem list "rspec-rails" --remote
This will list a few gems, but you will see the rspec-rails gem in the list, along with a version number (at time of writing, 2.5.0). So now we can add this to our Gemfile (don’t forget to put it in your test and development groups!):
gem 'rspec-rails', '~> 2.5.0'
So what is the little tilde arrow thingy for? Well, it means to use any version in the 2.5.0 minor release, but not 2.6. So it’ll use 2.5.1, 2.5.2, 2.5.3, etc… but it won’t upgrade to a new minor release. This is because breaking changes are much more likely to occur when switching between minor releases and these updates should occur consciously.
Now we just need to run "bundle" and watch as RSpec is installed:
Okay, RSpec should have installed itself into your gemset. Now if you run:
You’ll see that RSpec has created a generator for you called "rspec:install". We are going to run this generator:
rails g rspec:install
This will create a few files in your app, but what you really care about is the "spec" folder. This is where you will put all of your RSpec tests, also called "specs" or "specifications". Now what we need to do is to tell Rails to use RSpec when generating tests, and not generate tests using its built in framework Test::Unit. (you can stick with Test::Unit if you want, no harm done, but you’re on your own in this post)
Now that RSpec is installed, we need to tell rails to use RSpec as the default test framework when generating tests. This will happen inside of the application.rb file. To do this we just need to add the following code inside of our RailsBaby module (in application.rb):
config.generators do |g| g.test_framework :rspec end
You can just put it in at the end of the module. Okay, so now we have RSpec ready to go, let’s go ahead run it with no tests just so see how it is going to work.
To run RSpec we will execute this from the console:
At this point you haven’t seen rake yet, and we won’t delve into it yet, but I’ll just say that it is "make" for Ruby. It is a DSL for creating build scripts that are written entirely in Ruby. It can be used to automate just about any task.
After you ran that command, you probably just got an error complaining about a file called "schema.rb" and something about a database. In order for us to get past this error we are going to need to run this command:
I’ll get back to that command in a bit, but for now I’ll just say that it initializes your database. What database? Just go check out your Gemfile. Rails includes support for SQLite by default, so you already have a database without even knowing it! How is that for sweet?
Go ahead now and run "rake spec" again. This time you should see a message that RSpec didn’t find any specs to run. Which is good, because that means you have it running!
Creating Some Content
If you’ve looked into Rails before, you have probably heard of this thing they call "scaffolding". Scaffolding is simply generated code that creates a simple set of views, controller and model which have the basic CRUD operations.
Many people will tell you to avoid scaffolding, and that is probably good advice, but when learning Rails I found it to be a good starting point for creating our first end to end request. This way we aren’t Googling and hunting for every little piece of code to put in. Since our application is called Rails Baby, let’s go ahead and generate a controller, model, and views for our babies. Just run this command:
rails g scaffold Baby name:string age:integer
Here we are running the scaffold generator with our model name "Baby" and two attributes, one called "name" and one called "age". We have also specified the types for each of our attributes. Once we run this command you’ll see a huge amount of stuff generated, and this is usually why people say that using scaffolding is bad, because it generates a ton of so-so code that you don’t want or don’t understand. We’ll get back to that in a second.
But first let’s look at some of the files which were generated when we ran the previous command.
- A file in the /db/migrate folder: This is called a migration, and it is what creates your database schema. We will look at this next.
- A baby_spec.rb file: This is a test file for our baby model.
- Some code into the route file: This route doesn’t look like anything we have seen before, again we will look at this in a bit.
- A babies_controller.rb file. Notice that Rails properly pluralized the file name. Cool stuff. This can be customized in the /config/initializers/inflections.rb file.
- A bunch of views for the babies.
- Specs for the views.
- A request spec: This is a spec that operates at the request level and allows you to do some integration testing with RSpec.
The first thing that we are going to look at is the migration file. Migrations are a way to version your database. Go ahead and open the file and look at it. In this file you’ll find this:
class CreateBabies < ActiveRecord::Migration def self.up create_table :babies do |t| t.string :name t.integer :age t.timestamps end end def self.down drop_table :babies end end
As you can see, there are two methods here, an up and a down. "up" is the method that creates the database, in this case the babies table. "down" is the method that undoes the migration. In this case it drops the babies table. The code you are seeing here is an api supplied by Rails to allow the creation of databases to be abstracted from the underlying database.
The general concept of migrations is that I can create these migration files and execute them locally to modify my database. Then when I go to commit my code, I will check in my migration files along with my source. When someone checks code out, they will run this command:
It will execute the "up" method on all of the migrations that haven’t been run yet (it stores the executed migrations ids in the database). It runs the files in order based on the filename. You probably noticed the timestamp that is generated in the migration filename when it was created. When you executed the above command you should see output in the console verifying that the babies table has been created. Now that we have created the needed tables, instead of pulling up the app just yet let’s first take a look at the model that we just generated.
The model that we generated is fairly simple, just look in /app/models/baby.rb:
class Baby < ActiveRecord::Base end
Wow, nothing there! But didn’t we just add attributes to this class when we generated it? Yes, we did, but those were used for the migration. The model class itself doesn’t care what attributes it has, these are create dynamically from the database table.
We need to add one thing to our model which some might see as a pain, but it is absolutely essential. The problem is "over posting" and without some additional code, Rails is vulnerable to this flaw. I wrote about this before with ASP.NET MVC. In Rails there is a fairly simple solution that will work for you most of the time, it is to change your model to look like this:
class Baby < ActiveRecord::Base attr_accessible :name, :age end
Now, whenever we create a model from form post parameters, only certain attributes can be assigned. All other parameters will have to be explicitly assigned. This will keep a hacker from "over posting" on your model and overwriting attributes that you don’t want them to be able to overwrite.
There is even a way to specify different sets of accessible attributes based on some state in the application, but that is a bit out of scope for what we are doing here. Go check out the docs for the MassAssignmentSecurity module.
Testing The Model
We know that we created a name and an age, so let’s just start from there. I want to add some simple validations onto my model to make sure that we aren’t allowing silly values to be entered. For instance, we want to require a name and we don’t want age to be less than zero. Sounds reasonable. First, let’s go ahead and find the spec that was generated for the Baby model and add a few pending specs to it. Go look in /spec/models/baby_spec.rb. At this point this file should be empty, but let’s go ahead and make it look like this:
require 'spec_helper' describe Baby do it "is valid with proper values" it "is not valid without a name" it "is not valid without an age" it "is not valid with an age less than zero" end
Here we have now defined four pending specs. If we ran RSpec now (with the "rake spec" command) it would tell us that we have four pending specs:
So let’s go ahead and fill out the first spec, we just want to create a baby model with some valid attributes, and then check to see if it is valid. RSpec has a "should" method that we can call on the model and then we can pass it a matcher:
it "is valid with proper values" do baby = Baby.new name:"Billy", age:12 baby.should be_valid end
In this test we are just creating a baby and giving it a valid name and age. In case you don’t recognize the synax, I am just calling "new" and then passing a Ruby 1.9 hash (the syntax has been updated a bit so that it looks more like JSON) which assigns values to each attribute. Next we are just checking that the model is valid using the "be_valid" matcher provided by RSpec. RSpec has a ton of these matchers, you can check out the docs if you want to see more, but we will probably talk about more of them in the future.
In the meantime, let’s go ahead and fill out the rest of those specs:
it "is not valid without a name" do baby = Baby.new age:12 baby.should_not be_valid end it "is not valid without an age" do baby = Baby.new name:"Billy" baby.should_not be_valid end it "is not valid with an age less than zero" do baby = Baby.new name:"Billy", age:-12 baby.should_not be_valid end
You might be thinking "argh! These tests are brittle and repetitive!" And you would be correct! Let’s first get these tests passing, and then we are going to look at a gem called "Factory Girl" in order to clean up this mess.
If you ran the tests now, you’d see that all of them fail, except for the first one. We need to get some validation into this model, which you can guess is a built in piece of Active Record. In order to accomplish this, we only need to annotate the Rails model in this way:
class Baby < ActiveRecord::Base validates_presence_of :name validates_numericality_of :age, :only_integer => true, :greater_than => 0 end
Very easy. Rails has a good number of validations, and they are also extensible. If you want to dig a lot deeper into Rails validations, you should check out the Rails Guide on validations.
The first validation "validates_presence_of" just makes sure that the value is there and isn’t whitespace. The second validation makes sure that the value is a number, only an integer, and is greater than zero. This should satisfy all of our specs, so we are good to go. Just go ahead and run RSpec again (remember "rake spec") and now all of your tests should be passing.
Cleaning Up The Tests With Factory Girl
I mentioned earlier that our tests sucked. They suck because we are building a new baby in every test, and if anything changes, like an attribute being added or removed, these tests will start failing. And you’ll have to go and touch each and every one. This might not suck too much right now, but in the future when you have 1000 tests it will really suck.
Factory Girl lets us easily build instances of our classes to use for testing. Rails has a built in feature called "fixtures" for this same purpose, but Factory Girl is much more flexible. The first step is to get the "factory_girl_rails" gem installed by putting it in our Gemfile:
gem 'factory_girl_rails', '~> 1.0.1'
Now run the "bundle" command to install it. Once it is installed you can go to your "spec" folder and create a "factories" folder. In this folder create a file called "baby_factories.rb". In this file we are going to put this code:
Factory.define :baby do |f| f.name "Billy" f.age 12 end
This just creates a factory for us called "baby" and it automatically uses the naming to guess the model class that it will use. In one of our tests, if we wanted to just replace the code inline, we could do this:
it "is valid with proper values" do baby = Factory.build(:baby) baby.should be_valid end
Notice the use of "Factory.build(:baby)", most Factory Girl tutorials you will see will just have "Factory(:baby)". The key difference here is that calling "Factory.build" does not save the model to the database, whereas calling "Factory" does. Depending on what kind of testing you are doing, use one or the other.
And you see that we have replaced where we were originally specifying our class explicitly. But we could arguably do a little bit better than this by using the RSpec’s "before(:each)" method and creating the baby object from the factory before each spec is run:
before(:each) do @baby = Factory.build(:baby) end it "is valid with proper values" do @baby.should be_valid end
In later tests, you could just override the needed attribute values for your test:
it "is not valid without a name" do @baby.name = nil @baby.should_not be_valid end
Although some people would prefer to specify the factory in every test, because you can just override attributes on creation:
it "is not valid without a name" do baby = Factory.build(:baby, name:nil) baby.should_not be_valid end
I guess it depends on what kinds of tests you were writing as to which form is more useful. I would recommend considering it on a case by case basis.
Wrapping It Up
In this installment of my Rails 3 baby steps series we learned a good bit about testing. After creating the model in the last entry we setup RSpec and started using it to run some tests against it. Then we added a few small validations to our model and made sure those were working. Finally we cleaned up our tests a bit by including Factory Girl. I really hope you enjoyed this entry in the series, and I hope you’ll come back and check out the next entry where we will dive into a full CRUD controller and how it works.